Merge branch '3.4' into 4.4

* 3.4:
  [Yaml] Fix for #36624; Allow PHP constant as first key in block
  Use PHPUnit 9.3 on php 8.
  fix mapping errors from unmapped forms
  [Validator] Add target guards for Composite nested constraints
This commit is contained in:
Fabien Potencier 2020-08-13 08:22:32 +02:00
commit a379051c82
11 changed files with 215 additions and 25 deletions

View File

@ -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;')) { 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_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) { } elseif (\PHP_VERSION_ID >= 70000) {
putenv('SYMFONY_PHPUNIT_VERSION=6.5'); putenv('SYMFONY_PHPUNIT_VERSION=6.5');
} }

View File

@ -231,13 +231,6 @@ class ViolationMapper implements ViolationMapperInterface
// Form inherits its parent data // Form inherits its parent data
// Cut the piece out of the property path and proceed // Cut the piece out of the property path and proceed
$propertyPathBuilder->remove($i); $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 { } else {
/* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */ /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
$propertyPath = $scope->getPropertyPath(); $propertyPath = $scope->getPropertyPath();

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -22,6 +22,7 @@ use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigBuilder; use Symfony\Component\Form\FormConfigBuilder;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Tests\Extension\Validator\ViolationMapper\Fixtures\Issue;
use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationInterface;
@ -70,12 +71,12 @@ class ViolationMapperTest extends TestCase
$this->params = ['foo' => 'bar']; $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, [ $config = new FormConfigBuilder($name, $dataClass, $this->dispatcher, [
'error_mapping' => $errorMapping, 'error_mapping' => $errorMapping,
]); ] + $options);
$config->setMapped(true); $config->setMapped(isset($options['mapped']) ? $options['mapped'] : true);
$config->setInheritData($inheritData); $config->setInheritData($inheritData);
$config->setPropertyPath($propertyPath); $config->setPropertyPath($propertyPath);
$config->setCompound(true); $config->setCompound(true);
@ -91,12 +92,9 @@ class ViolationMapperTest extends TestCase
return new Form($config); return new Form($config);
} }
/** protected function getConstraintViolation($propertyPath, $root = null): ConstraintViolation
* @param $propertyPath
*/
protected function getConstraintViolation($propertyPath): 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 protected function getFormError(ConstraintViolationInterface $violation, FormInterface $form): FormError
@ -107,6 +105,33 @@ class ViolationMapperTest extends TestCase
return $error; 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() public function testMapToFormInheritingParentDataIfDataDoesNotMatch()
{ {
$violation = $this->getConstraintViolation('children[address].data.foo'); $violation = $this->getConstraintViolation('children[address].data.foo');

View File

@ -136,6 +136,17 @@ abstract class Composite extends Constraint
*/ */
abstract protected function getCompositeOption(); abstract protected function getCompositeOption();
/**
* @internal Used by metadata
*
* @return Constraint[]
*/
public function getNestedContraints()
{
/* @var Constraint[] $nestedConstraints */
return $this->{$this->getCompositeOption()};
}
/** /**
* Initializes the nested constraints. * Initializes the nested constraints.
* *

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Mapping; namespace Symfony\Component\Validator\Mapping;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Composite;
use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
@ -173,9 +174,7 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface
*/ */
public function addConstraint(Constraint $constraint) public function addConstraint(Constraint $constraint)
{ {
if (!\in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets())) { $this->checkConstraint($constraint);
throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on classes.', \get_class($constraint)));
}
if ($constraint instanceof Traverse) { if ($constraint instanceof Traverse) {
if ($constraint->traverse) { if ($constraint->traverse) {
@ -487,4 +486,17 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface
$this->members[$property][] = $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_class($constraint)));
}
if ($constraint instanceof Composite) {
foreach ($constraint->getNestedContraints() as $nestedContraint) {
$this->checkConstraint($nestedContraint);
}
}
}
} }

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Mapping; namespace Symfony\Component\Validator\Mapping;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Composite;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
/** /**
@ -71,9 +72,7 @@ abstract class MemberMetadata extends GenericMetadata implements PropertyMetadat
*/ */
public function addConstraint(Constraint $constraint) public function addConstraint(Constraint $constraint)
{ {
if (!\in_array(Constraint::PROPERTY_CONSTRAINT, (array) $constraint->getTargets())) { $this->checkConstraint($constraint);
throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on properties or getters.', \get_class($constraint)));
}
parent::addConstraint($constraint); parent::addConstraint($constraint);
@ -181,4 +180,17 @@ abstract class MemberMetadata extends GenericMetadata implements PropertyMetadat
* @return \ReflectionMethod|\ReflectionProperty The reflection instance * @return \ReflectionMethod|\ReflectionProperty The reflection instance
*/ */
abstract protected function newReflectionMember($objectOrClassName); 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_class($constraint)));
}
if ($constraint instanceof Composite) {
foreach ($constraint->getNestedContraints() as $nestedContraint) {
$this->checkConstraint($nestedContraint);
}
}
}
} }

View File

@ -13,8 +13,11 @@ namespace Symfony\Component\Validator\Tests\Mapping;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Composite;
use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB;
use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint; use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint;
@ -52,6 +55,20 @@ class ClassMetadataTest extends TestCase
$this->metadata->addConstraint(new PropertyConstraint()); $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() public function testAddPropertyConstraints()
{ {
$this->metadata->addPropertyConstraint('firstName', new ConstraintA()); $this->metadata->addPropertyConstraint('firstName', new ConstraintA());
@ -311,3 +328,23 @@ class ClassMetadataTest extends TestCase
$this->assertCount(0, $this->metadata->getPropertyMetadata('foo'), '->getPropertyMetadata() returns an empty collection if no metadata is configured for the given property'); $this->assertCount(0, $this->metadata->getPropertyMetadata('foo'), '->getPropertyMetadata() returns an empty collection if no metadata is configured for the given property');
} }
} }
class ClassCompositeConstraint extends Composite
{
public $nested;
public function getDefaultOption()
{
return $this->getCompositeOption();
}
protected function getCompositeOption()
{
return 'nested';
}
public function getTargets()
{
return [self::CLASS_CONSTRAINT];
}
}

View File

@ -12,11 +12,16 @@
namespace Symfony\Component\Validator\Tests\Mapping; namespace Symfony\Component\Validator\Tests\Mapping;
use PHPUnit\Framework\TestCase; 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\Constraints\Valid;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Mapping\MemberMetadata; use Symfony\Component\Validator\Mapping\MemberMetadata;
use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB;
use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint;
class MemberMetadataTest extends TestCase class MemberMetadataTest extends TestCase
{ {
@ -43,6 +48,34 @@ class MemberMetadataTest extends TestCase
$this->metadata->addConstraint(new ClassConstraint()); $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() public function testSerialize()
{ {
$this->metadata->addConstraint(new ConstraintA(['property1' => 'A'])); $this->metadata->addConstraint(new ConstraintA(['property1' => 'A']));
@ -82,3 +115,18 @@ class TestMemberMetadata extends MemberMetadata
{ {
} }
} }
class PropertyCompositeConstraint extends Composite
{
public $nested;
public function getDefaultOption()
{
return $this->getCompositeOption();
}
protected function getCompositeOption()
{
return 'nested';
}
}

View File

@ -176,8 +176,12 @@ class Parser
$this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags) $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
); );
} else { } else {
if (isset($values['leadspaces']) if (
&& self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches) isset($values['leadspaces'])
&& (
'!' === $values['value'][0]
|| self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
)
) { ) {
// this is a compact notation element, add to next block and parse // this is a compact notation element, add to next block and parse
$block = $values['value']; $block = $values['value'];

View File

@ -2077,6 +2077,34 @@ YAML;
$this->parser->parse('!php/const:App\Kernel::SEMART_VERSION', Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_CONSTANT); $this->parser->parse('!php/const:App\Kernel::SEMART_VERSION', Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_CONSTANT);
} }
public function testPhpConstantTagMappingAsScalarKey()
{
$yaml = <<<YAML
map1:
- foo: 'value_0'
!php/const 'Symfony\Component\Yaml\Tests\B::BAR': 'value_1'
map2:
- !php/const 'Symfony\Component\Yaml\Tests\B::FOO': 'value_0'
bar: 'value_1'
YAML;
$this->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 = <<<YAML
map1:
- !!str 0: 'value_0'
!!str 1: 'value_1'
YAML;
$this->assertSame([
'map1' => [['0' => 'value_0', '1' => 'value_1']],
], $this->parser->parse($yaml));
}
public function testMergeKeysWhenMappingsAreParsedAsObjects() public function testMergeKeysWhenMappingsAreParsedAsObjects()
{ {
$yaml = <<<YAML $yaml = <<<YAML
@ -2339,7 +2367,7 @@ YAML;
parameters: parameters:
abc abc
# Comment # Comment
YAML; YAML;
$this->assertSame(['parameters' => 'abc'], $this->parser->parse($yaml)); $this->assertSame(['parameters' => 'abc'], $this->parser->parse($yaml));