Skip to content

Commit

Permalink
Merge branch '5.0'
Browse files Browse the repository at this point in the history
* 5.0:
  [HttpFoundation] workaround PHP bug in the session module
  [SecurityBundle] fix accepting env vars in remember-me configurations
  [Form] Fixed handling groups sequence validation
  [Mime] Ensure proper line-ending for SMIME
  [Cache] Avoid memory leak in TraceableAdapter::reset()
  • Loading branch information
nicolas-grekas committed Apr 18, 2020
2 parents 23f5070 + efbe752 commit 69b6c90
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 38 deletions.
10 changes: 3 additions & 7 deletions src/Symfony/Component/Cache/Adapter/TraceableAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,11 @@ public function prune()
*/
public function reset()
{
if (!$this->pool instanceof ResetInterface) {
return;
}
$event = $this->start(__FUNCTION__);
try {
if ($this->pool instanceof ResetInterface) {
$this->pool->reset();
} finally {
$event->end = microtime(true);
}

$this->clearCalls();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
*/
class FormValidator extends ConstraintValidator
{
private $resolvedGroups;

/**
* {@inheritdoc}
*/
Expand All @@ -44,44 +46,70 @@ public function validate($form, Constraint $formConstraint)

if ($form->isSubmitted() && $form->isSynchronized()) {
// Validate the form data only if transformation succeeded
$groups = self::getValidationGroups($form);
$groups = $this->getValidationGroups($form);

if (!$groups) {
return;
}

$data = $form->getData();

// Validate the data against its own constraints
if ($form->isRoot() && (\is_object($data) || \is_array($data))) {
if (($groups && \is_array($groups)) || ($groups instanceof GroupSequence && $groups->groups)) {
$validator->atPath('data')->validate($form->getData(), null, $groups);
}
}
$validateDataGraph = $form->isRoot()
&& (\is_object($data) || \is_array($data))
&& (($groups && \is_array($groups)) || ($groups instanceof GroupSequence && $groups->groups))
;

// Validate the data against the constraints defined
// in the form
// Validate the data against the constraints defined in the form
/** @var Constraint[] $constraints */
$constraints = $config->getOption('constraints', []);

if ($groups instanceof GroupSequence) {
$validator->atPath('data')->validate($form->getData(), $constraints, $groups);
// Otherwise validate a constraint only once for the first
// matching group
foreach ($groups as $group) {
if (\in_array($group, $formConstraint->groups)) {
$validator->atPath('data')->validate($form->getData(), $formConstraint, $group);
if (\count($this->context->getViolations()) > 0) {
break;
// Validate the data, the form AND nested fields in sequence
$violationsCount = $this->context->getViolations()->count();
$fieldPropertyPath = \is_object($data) ? 'children[%s]' : 'children%s';
$hasChildren = $form->count() > 0;
$this->resolvedGroups = $hasChildren ? new \SplObjectStorage() : null;

foreach ($groups->groups as $group) {
if ($validateDataGraph) {
$validator->atPath('data')->validate($data, null, $group);
}

if ($groupedConstraints = self::getConstraintsInGroups($constraints, $group)) {
$validator->atPath('data')->validate($data, $groupedConstraints, $group);
}

foreach ($form->all() as $field) {
if ($field->isSubmitted()) {
// remember to validate this field is one group only
// otherwise resolving the groups would reuse the same
// sequence recursively, thus some fields could fail
// in different steps without breaking early enough
$this->resolvedGroups[$field] = (array) $group;
$validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $formConstraint);
}
}

if ($violationsCount < $this->context->getViolations()->count()) {
break;
}
}

if ($hasChildren) {
// destroy storage at the end of the sequence to avoid memory leaks
$this->resolvedGroups = null;
}
} else {
if ($validateDataGraph) {
$validator->atPath('data')->validate($data, null, $groups);
}

$groupedConstraints = [];

foreach ($constraints as $constraint) {
// For the "Valid" constraint, validate the data in all groups
if ($constraint instanceof Valid) {
$validator->atPath('data')->validate($form->getData(), $constraint, $groups);
$validator->atPath('data')->validate($data, $constraint, $groups);

continue;
}
Expand All @@ -101,7 +129,7 @@ public function validate($form, Constraint $formConstraint)
}

foreach ($groupedConstraints as $group => $constraint) {
$validator->atPath('data')->validate($form->getData(), $constraint, $group);
$validator->atPath('data')->validate($data, $constraint, $group);
}
}
} elseif (!$form->isSynchronized()) {
Expand Down Expand Up @@ -160,7 +188,7 @@ public function validate($form, Constraint $formConstraint)
*
* @return string|GroupSequence|(string|GroupSequence)[] The validation groups
*/
private static function getValidationGroups(FormInterface $form)
private function getValidationGroups(FormInterface $form)
{
// Determine the clicked button of the complete form tree
$clickedButton = null;
Expand All @@ -184,6 +212,10 @@ private static function getValidationGroups(FormInterface $form)
return self::resolveValidationGroups($groups, $form);
}

if (isset($this->resolvedGroups[$form])) {
return $this->resolvedGroups[$form];
}

$form = $form->getParent();
} while (null !== $form);

Expand All @@ -209,4 +241,11 @@ private static function resolveValidationGroups($groups, FormInterface $form)

return (array) $groups;
}

private static function getConstraintsInGroups($constraints, $group)
{
return array_filter($constraints, static function (Constraint $constraint) use ($group) {
return \in_array($group, $constraint->groups, true);
});
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/Form/Resources/config/validation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<class name="Symfony\Component\Form\Form">
<constraint name="Symfony\Component\Form\Extension\Validator\Constraints\Form" />
<property name="children">
<constraint name="Valid" />
<constraint name="Valid" />
</property>
</class>
</constraint-mapping>
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,8 @@ public function testHandleGroupSequenceValidationGroups()
$form = $this->getCompoundForm($object, $options);
$form->submit([]);

$this->expectValidateAt(0, 'data', $object, new GroupSequence(['group1', 'group2']));
$this->expectValidateAt(1, 'data', $object, new GroupSequence(['group1', 'group2']));
$this->expectValidateAt(0, 'data', $object, 'group1');
$this->expectValidateAt(1, 'data', $object, 'group2');

$this->validator->validate($form, new Form());

Expand Down Expand Up @@ -801,6 +801,39 @@ public function testCompositeConstraintValidatedInEachGroup()
$this->assertSame('data[field2]', $context->getViolations()[1]->getPropertyPath());
}

public function testCompositeConstraintValidatedInSequence()
{
$form = $this->getCompoundForm([], [
'constraints' => [
new Collection([
'field1' => new NotBlank([
'groups' => ['field1'],
]),
'field2' => new NotBlank([
'groups' => ['field2'],
]),
]),
],
'validation_groups' => new GroupSequence(['field1', 'field2']),
])
->add($this->getForm('field1'))
->add($this->getForm('field2'))
;

$form->submit([
'field1' => '',
'field2' => '',
]);

$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
$this->validator->initialize($context);
$this->validator->validate($form, new Form());

$this->assertCount(1, $context->getViolations());
$this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage());
$this->assertSame('data[field1]', $context->getViolations()[0]->getPropertyPath());
}

protected function createValidator()
{
return new FormValidator();
Expand All @@ -823,7 +856,7 @@ private function getForm($name = 'name', $dataClass = null, array $options = [])

private function getCompoundForm($data, array $options = [])
{
return $this->getBuilder('name', \get_class($data), $options)
return $this->getBuilder('name', \is_object($data) ? \get_class($data) : null, $options)
->setData($data)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@
namespace Symfony\Component\Form\Tests\Extension\Validator\Type;

use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
use Symfony\Component\Form\Tests\Extension\Core\Type\FormTypeTest;
use Symfony\Component\Form\Tests\Extension\Core\Type\TextTypeTest;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Form\Tests\Fixtures\Author;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
use Symfony\Component\Validator\Validation;

class FormTypeValidatorExtensionTest extends BaseValidatorExtensionTest
Expand Down Expand Up @@ -64,14 +68,71 @@ public function testGroupSequenceWithConstraintsOption()
->add('field', TextTypeTest::TESTED_TYPE, [
'constraints' => [
new Length(['min' => 10, 'allowEmptyString' => true, 'groups' => ['First']]),
new Email(['groups' => ['Second']]),
new NotBlank(['groups' => ['Second']]),
],
])
;

$form->submit(['field' => 'wrong']);

$this->assertCount(1, $form->getErrors(true));
$errors = $form->getErrors(true);

$this->assertCount(1, $errors);
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
}

public function testManyFieldsGroupSequenceWithConstraintsOption()
{
$allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];

$formMetadata = new ClassMetadata(Form::class);
$authorMetadata = (new ClassMetadata(Author::class))
->addPropertyConstraint('firstName', new NotBlank(['groups' => 'Second']))
;
$metadataFactory = $this->createMock(MetadataFactoryInterface::class);
$metadataFactory->expects($this->any())
->method('getMetadataFor')
->willReturnCallback(static function ($classOrObject) use ($formMetadata, $authorMetadata) {
if (Author::class === $classOrObject || $classOrObject instanceof Author) {
return $authorMetadata;
}

if (Form::class === $classOrObject || $classOrObject instanceof Form) {
return $formMetadata;
}

return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : \get_class($classOrObject));
})
;

$validator = Validation::createValidatorBuilder()
->setMetadataFactory($metadataFactory)
->getValidator()
;
$form = Forms::createFormFactoryBuilder()
->addExtension(new ValidatorExtension($validator))
->getFormFactory()
->create(FormTypeTest::TESTED_TYPE, new Author(), (['validation_groups' => new GroupSequence(['First', 'Second'])]))
->add('firstName', TextTypeTest::TESTED_TYPE)
->add('lastName', TextTypeTest::TESTED_TYPE, [
'constraints' => [
new Length(['min' => 10, 'groups' => ['First']] + $allowEmptyString),
],
])
->add('australian', TextTypeTest::TESTED_TYPE, [
'constraints' => [
new NotBlank(['groups' => ['Second']]),
],
])
;

$form->submit(['firstName' => '', 'lastName' => 'wrong_1', 'australian' => '']);

$errors = $form->getErrors(true);

$this->assertCount(1, $errors);
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
$this->assertSame('children[lastName].data', $errors[0]->getCause()->getPropertyPath());
}

protected function createForm(array $options = [])
Expand Down
Loading

0 comments on commit 69b6c90

Please sign in to comment.