diff --git a/phpunit b/phpunit index 713594fc19ed..2b4412dc4d4f 100755 --- a/phpunit +++ b/phpunit @@ -12,7 +12,11 @@ if (!getenv('SYMFONY_PHPUNIT_VERSION')) { if (false === getenv('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT') && false !== strpos(@file_get_contents(__DIR__.'/src/Symfony/Component/HttpKernel/Kernel.php'), 'const MAJOR_VERSION = 3;')) { putenv('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1'); } - putenv('SYMFONY_PHPUNIT_VERSION=8.3'); + if (\PHP_VERSION_ID >= 80000) { + putenv('SYMFONY_PHPUNIT_VERSION=9.3'); + } else { + putenv('SYMFONY_PHPUNIT_VERSION=8.3'); + } } elseif (\PHP_VERSION_ID >= 70000) { putenv('SYMFONY_PHPUNIT_VERSION=6.5'); } diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php index 22ec19c9945e..12abef214bd7 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -231,13 +231,6 @@ private function reconstructPath(ViolationPath $violationPath, FormInterface $or // Form inherits its parent data // Cut the piece out of the property path and proceed $propertyPathBuilder->remove($i); - } elseif (!$scope->getConfig()->getMapped()) { - // Form is not mapped - // Set the form as new origin and strip everything - // we have so far in the path - $origin = $scope; - $propertyPathBuilder->remove(0, $i + 1); - $i = 0; } else { /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */ $propertyPath = $scope->getPropertyPath(); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/Fixtures/Issue.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/Fixtures/Issue.php new file mode 100644 index 000000000000..62b6a4e77d15 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/Fixtures/Issue.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Validator\ViolationMapper\Fixtures; + +class Issue +{ +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php index be875fb322b2..100c54ad462e 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Form\FormConfigBuilder; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\Tests\Extension\Validator\ViolationMapper\Fixtures\Issue; use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationInterface; @@ -70,12 +71,12 @@ protected function setUp(): void $this->params = ['foo' => 'bar']; } - protected function getForm($name = 'name', $propertyPath = null, $dataClass = null, $errorMapping = [], $inheritData = false, $synchronized = true) + protected function getForm($name = 'name', $propertyPath = null, $dataClass = null, $errorMapping = [], $inheritData = false, $synchronized = true, array $options = []) { $config = new FormConfigBuilder($name, $dataClass, $this->dispatcher, [ 'error_mapping' => $errorMapping, - ]); - $config->setMapped(true); + ] + $options); + $config->setMapped(isset($options['mapped']) ? $options['mapped'] : true); $config->setInheritData($inheritData); $config->setPropertyPath($propertyPath); $config->setCompound(true); @@ -91,12 +92,9 @@ function () { throw new TransformationFailedException(); } return new Form($config); } - /** - * @param $propertyPath - */ - protected function getConstraintViolation($propertyPath): ConstraintViolation + protected function getConstraintViolation($propertyPath, $root = null): ConstraintViolation { - return new ConstraintViolation($this->message, $this->messageTemplate, $this->params, null, $propertyPath, null); + return new ConstraintViolation($this->message, $this->messageTemplate, $this->params, $root, $propertyPath, null); } protected function getFormError(ConstraintViolationInterface $violation, FormInterface $form): FormError @@ -107,6 +105,33 @@ protected function getFormError(ConstraintViolationInterface $violation, FormInt return $error; } + public function testMappingErrorsWhenFormIsNotMapped() + { + $form = $this->getForm('name', null, Issue::class, [ + 'child1' => 'child2', + ]); + + $violation = $this->getConstraintViolation('children[child1].data', $form); + + $child1Form = $this->getForm('child1', null, null, [], false, true, [ + 'mapped' => false, + ]); + $child2Form = $this->getForm('child2', null, null, [], false, true, [ + 'mapped' => false, + ]); + + $form->add($child1Form); + $form->add($child2Form); + + $form->submit([]); + + $this->mapper->mapViolation($violation, $form); + + $this->assertCount(0, $form->getErrors()); + $this->assertCount(0, $form->get('child1')->getErrors()); + $this->assertCount(1, $form->get('child2')->getErrors()); + } + public function testMapToFormInheritingParentDataIfDataDoesNotMatch() { $violation = $this->getConstraintViolation('children[address].data.foo'); diff --git a/src/Symfony/Component/Validator/Constraints/Composite.php b/src/Symfony/Component/Validator/Constraints/Composite.php index 57275fe94aca..0d9bb8ecfe66 100644 --- a/src/Symfony/Component/Validator/Constraints/Composite.php +++ b/src/Symfony/Component/Validator/Constraints/Composite.php @@ -136,6 +136,17 @@ public function addImplicitGroupName($group) */ abstract protected function getCompositeOption(); + /** + * @internal Used by metadata + * + * @return Constraint[] + */ + public function getNestedContraints() + { + /* @var Constraint[] $nestedConstraints */ + return $this->{$this->getCompositeOption()}; + } + /** * Initializes the nested constraints. * diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 41520ccb1998..7aa90fe492c0 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Cascade; +use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; @@ -186,9 +187,7 @@ public function getDefaultGroup() */ public function addConstraint(Constraint $constraint) { - if (!\in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets())) { - throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on classes.', get_debug_type($constraint))); - } + $this->checkConstraint($constraint); if ($constraint instanceof Traverse) { if ($constraint->traverse) { @@ -502,4 +501,17 @@ private function addPropertyMetadata(PropertyMetadataInterface $metadata) $this->members[$property][] = $metadata; } + + private function checkConstraint(Constraint $constraint) + { + if (!\in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets(), true)) { + throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on classes.', get_debug_type($constraint))); + } + + if ($constraint instanceof Composite) { + foreach ($constraint->getNestedContraints() as $nestedContraint) { + $this->checkConstraint($nestedContraint); + } + } + } } diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 68196350f41d..e3ff1f356d87 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** @@ -71,9 +72,7 @@ public function __construct(string $class, string $name, string $property) */ public function addConstraint(Constraint $constraint) { - if (!\in_array(Constraint::PROPERTY_CONSTRAINT, (array) $constraint->getTargets())) { - throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on properties or getters.', get_debug_type($constraint))); - } + $this->checkConstraint($constraint); parent::addConstraint($constraint); @@ -181,4 +180,17 @@ public function getReflectionMember($objectOrClassName) * @return \ReflectionMethod|\ReflectionProperty The reflection instance */ abstract protected function newReflectionMember($objectOrClassName); + + private function checkConstraint(Constraint $constraint) + { + if (!\in_array(Constraint::PROPERTY_CONSTRAINT, (array) $constraint->getTargets(), true)) { + throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on properties or getters.', get_debug_type($constraint))); + } + + if ($constraint instanceof Composite) { + foreach ($constraint->getNestedContraints() as $nestedContraint) { + $this->checkConstraint($nestedContraint); + } + } + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/RangeTest.php b/src/Symfony/Component/Validator/Tests/Constraints/RangeTest.php index ee99c8deef74..1062977d8d6e 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/RangeTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/RangeTest.php @@ -52,7 +52,7 @@ public function provideDeprecationTriggeredIfMinMaxAndMinMessageOrMaxMessageSet( /** * @group legacy - * @expectedDeprecation Since symfony/validator 4.4: minMessage and maxMessage are deprecated when min and max options are set together. Use notInRangeMessage instead. + * @expectedDeprecation Since symfony/validator 4.4: "minMessage" and "maxMessage" are deprecated when the "min" and "max" options are both set. Use "notInRangeMessage" instead. * @dataProvider provideDeprecationTriggeredIfMinMaxAndMinMessageOrMaxMessageSet */ public function testDeprecationTriggeredIfMinMaxAndMinMessageOrMaxMessageSet(array $options, bool $expectedDeprecatedMinMessageSet, bool $expectedDeprecatedMaxMessageSet) diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php index 9f0ab71b62ad..90133f75e491 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php @@ -14,11 +14,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Cascade; +use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Tests\Fixtures\CascadingEntity; +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\PropertyConstraint; @@ -56,6 +58,20 @@ public function testAddConstraintRequiresClassConstraints() $this->metadata->addConstraint(new PropertyConstraint()); } + public function testAddCompositeConstraintRejectsNestedPropertyConstraints() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint" cannot be put on classes.'); + + $this->metadata->addConstraint(new ClassCompositeConstraint([new PropertyConstraint()])); + } + + public function testAddCompositeConstraintAcceptsNestedClassConstraints() + { + $this->metadata->addConstraint($constraint = new ClassCompositeConstraint([new ClassConstraint()])); + $this->assertSame($this->metadata->getConstraints(), [$constraint]); + } + public function testAddPropertyConstraints() { $this->metadata->addPropertyConstraint('firstName', new ConstraintA()); @@ -347,3 +363,23 @@ public function testCascadeConstraint() ], $metadata->getConstrainedProperties()); } } + +class ClassCompositeConstraint extends Composite +{ + public $nested; + + public function getDefaultOption() + { + return $this->getCompositeOption(); + } + + protected function getCompositeOption() + { + return 'nested'; + } + + public function getTargets() + { + return [self::CLASS_CONSTRAINT]; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php index 73717de98e4e..55e030a2dcd2 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php @@ -12,11 +12,16 @@ namespace Symfony\Component\Validator\Tests\Mapping; use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\Collection; +use Symfony\Component\Validator\Constraints\Composite; +use Symfony\Component\Validator\Constraints\Required; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Mapping\MemberMetadata; 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\PropertyConstraint; class MemberMetadataTest extends TestCase { @@ -43,6 +48,34 @@ public function testAddConstraintRequiresClassConstraints() $this->metadata->addConstraint(new ClassConstraint()); } + public function testAddCompositeConstraintRejectsNestedClassConstraints() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Tests\Fixtures\ClassConstraint" cannot be put on properties or getters.'); + + $this->metadata->addConstraint(new PropertyCompositeConstraint([new ClassConstraint()])); + } + + public function testAddCompositeConstraintRejectsDeepNestedClassConstraints() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Tests\Fixtures\ClassConstraint" cannot be put on properties or getters.'); + + $this->metadata->addConstraint(new Collection(['field1' => new Required([new ClassConstraint()])])); + } + + public function testAddCompositeConstraintAcceptsNestedPropertyConstraints() + { + $this->metadata->addConstraint($constraint = new PropertyCompositeConstraint([new PropertyConstraint()])); + $this->assertSame($this->metadata->getConstraints(), [$constraint]); + } + + public function testAddCompositeConstraintAcceptsDeepNestedPropertyConstraints() + { + $this->metadata->addConstraint($constraint = new Collection(['field1' => new Required([new PropertyConstraint()])])); + $this->assertSame($this->metadata->getConstraints(), [$constraint]); + } + public function testSerialize() { $this->metadata->addConstraint(new ConstraintA(['property1' => 'A'])); @@ -82,3 +115,18 @@ protected function newReflectionMember($object): object { } } + +class PropertyCompositeConstraint extends Composite +{ + public $nested; + + public function getDefaultOption() + { + return $this->getCompositeOption(); + } + + protected function getCompositeOption() + { + return 'nested'; + } +} diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 1234ff43eebf..65b4fcffa41e 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -179,8 +179,12 @@ private function doParse(string $value, int $flags) $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags) ); } else { - if (isset($values['leadspaces']) - && self::preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P.+?))?\s*$#u', $this->trimTag($values['value']), $matches) + if ( + isset($values['leadspaces']) + && ( + '!' === $values['value'][0] + || self::preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P.+?))?\s*$#u', $this->trimTag($values['value']), $matches) + ) ) { // this is a compact notation element, add to next block and parse $block = $values['value']; diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 2a34d40cf72e..664acdb8b5df 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -2076,6 +2076,34 @@ public function testDeprecatedPhpConstantSyntax() $this->parser->parse('!php/const:App\Kernel::SEMART_VERSION', Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_CONSTANT); } + public function testPhpConstantTagMappingAsScalarKey() + { + $yaml = <<assertSame([ + 'map1' => [['foo' => 'value_0', 'bar' => 'value_1']], + 'map2' => [['foo' => 'value_0', 'bar' => 'value_1']], + ], $this->parser->parse($yaml, Yaml::PARSE_CONSTANT)); + } + + public function testTagMappingAsScalarKey() + { + $yaml = <<assertSame([ + 'map1' => [['0' => 'value_0', '1' => 'value_1']], + ], $this->parser->parse($yaml)); + } + public function testMergeKeysWhenMappingsAreParsedAsObjects() { $yaml = <<assertSame(['parameters' => 'abc'], $this->parser->parse($yaml));