Skip to content

Commit fbe76e9

Browse files
committed
[Form] Add constraints_from_* options
Apply constraints to field from an entity not mapped by the form data_class. Fix fabbot review Fix changelog to 6.4
1 parent 4813d66 commit fbe76e9

File tree

4 files changed

+181
-0
lines changed

4 files changed

+181
-0
lines changed

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
CHANGELOG
22
=========
33

4+
6.4
5+
---
6+
* Add `constraints_from_entity` and `constraints_from_property` option to `FormType`
7+
48
6.3
59
---
610

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Validator\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\Form\FormError;
16+
use Symfony\Component\Form\FormEvent;
17+
use Symfony\Component\Form\FormEvents;
18+
use Symfony\Component\Validator\ConstraintViolationInterface;
19+
use Symfony\Component\Validator\Validator\ValidatorInterface;
20+
21+
class ConstraintsFromListener implements EventSubscriberInterface
22+
{
23+
private ValidatorInterface $validator;
24+
25+
public static function getSubscribedEvents(): array
26+
{
27+
return [FormEvents::POST_SUBMIT => 'validateConstraints'];
28+
}
29+
30+
public function __construct(ValidatorInterface $validator)
31+
{
32+
$this->validator = $validator;
33+
}
34+
35+
public function validateConstraints(FormEvent $event): void
36+
{
37+
$form = $event->getForm();
38+
39+
$entity = $form->getConfig()->getOption('constraints_from_entity');
40+
41+
if (null !== $entity) {
42+
$property = $form->getConfig()->getOption('constraints_from_property') ?? $form->getName();
43+
44+
$violations = $this->validator->validatePropertyValue($entity, $property, $event->getData());
45+
/** @var ConstraintViolationInterface $violation */
46+
foreach ($violations as $violation) {
47+
$form->addError(
48+
new FormError(
49+
$violation->getMessage(),
50+
$violation->getMessageTemplate(),
51+
$violation->getParameters(),
52+
$violation->getPlural()
53+
)
54+
);
55+
}
56+
}
57+
}
58+
}

src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Form\Extension\Validator\Type;
1313

1414
use Symfony\Component\Form\Extension\Core\Type\FormType;
15+
use Symfony\Component\Form\Extension\Validator\EventListener\ConstraintsFromListener;
1516
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
1617
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
1718
use Symfony\Component\Form\FormBuilderInterface;
@@ -42,6 +43,7 @@ public function __construct(ValidatorInterface $validator, bool $legacyErrorMess
4243
*/
4344
public function buildForm(FormBuilderInterface $builder, array $options)
4445
{
46+
$builder->addEventSubscriber(new ConstraintsFromListener($this->validator));
4547
$builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper));
4648
}
4749

@@ -58,13 +60,17 @@ public function configureOptions(OptionsResolver $resolver)
5860
$resolver->setDefaults([
5961
'error_mapping' => [],
6062
'constraints' => [],
63+
'constraints_from_entity' => null,
64+
'constraints_from_property' => null,
6165
'invalid_message' => 'This value is not valid.',
6266
'invalid_message_parameters' => [],
6367
'allow_extra_fields' => false,
6468
'extra_fields_message' => 'This form should not contain extra fields.',
6569
]);
6670
$resolver->setAllowedTypes('constraints', [Constraint::class, Constraint::class.'[]']);
6771
$resolver->setNormalizer('constraints', $constraintsNormalizer);
72+
$resolver->setAllowedTypes('constraints_from_entity', ['string', 'null']);
73+
$resolver->setAllowedTypes('constraints_from_property', ['string', 'null']);
6874
}
6975

7076
public static function getExtendedTypes(): iterable

src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,119 @@ public function testInvalidMessage()
154154
$this->assertEquals('This value is not valid.', $form->getConfig()->getOption('invalid_message'));
155155
}
156156

157+
public function testConstraintsFromEntityValid()
158+
{
159+
// Maps firstName field to Author::firstName -> Length(3) constraint
160+
$form = $this->createFormForConstraintsFrom();
161+
162+
$form->submit(['firstName' => 'foo']);
163+
164+
$errors = $form->getErrors(true);
165+
166+
$this->assertCount(0, $errors);
167+
}
168+
169+
public function testConstraintsFromEntityEmpty()
170+
{
171+
// Maps firstName field to Author::firstName -> Length(3) constraint
172+
$form = $this->createFormForConstraintsFrom();
173+
174+
$form->submit(['firstName' => '']);
175+
176+
$errors = $form->getErrors(true);
177+
178+
$this->assertCount(1, $errors);
179+
}
180+
181+
public function testConstraintsFromEntityInvalid()
182+
{
183+
// Maps firstName field to Author::firstName -> Length(3) constraint
184+
$form = $this->createFormForConstraintsFrom();
185+
186+
$form->submit(['firstName' => 'foobar']);
187+
188+
$errors = $form->getErrors(true);
189+
190+
$this->assertCount(1, $errors);
191+
}
192+
193+
public function testConstraintsFromEntityCustomPropertyValid()
194+
{
195+
// Maps firstName field to Author::lastName -> Length(min: 5) constraint
196+
$form = $this->createFormForConstraintsFrom('lastName');
197+
198+
$form->submit(['firstName' => 'foobar']);
199+
200+
$errors = $form->getErrors(true);
201+
202+
$this->assertCount(0, $errors);
203+
}
204+
205+
public function testConstraintsFromEntityCustomPropertyEmpty()
206+
{
207+
// Maps firstName field to Author::lastName -> Length(min: 5) constraint
208+
$form = $this->createFormForConstraintsFrom('lastName');
209+
210+
$form->submit(['firstName' => '']);
211+
212+
$errors = $form->getErrors(true);
213+
214+
$this->assertCount(1, $errors);
215+
}
216+
217+
public function testConstraintsFromEntityCustomPropertyInvalid()
218+
{
219+
// Maps firstName field to Author::lastName -> Length(min: 5) constraint
220+
$form = $this->createFormForConstraintsFrom('lastName');
221+
222+
$form->submit(['firstName' => 'foo']);
223+
224+
$errors = $form->getErrors(true);
225+
226+
$this->assertCount(1, $errors);
227+
}
228+
229+
protected function createFormForConstraintsFrom(string $propertyName = null)
230+
{
231+
$formMetadata = new ClassMetadata(Form::class);
232+
$authorMetadata = (new ClassMetadata(Author::class))
233+
->addPropertyConstraint('firstName', new Length(3))
234+
->addPropertyConstraint('lastName', new Length(min: 5))
235+
;
236+
$metadataFactory = $this->createMock(MetadataFactoryInterface::class);
237+
$metadataFactory->expects($this->any())
238+
->method('getMetadataFor')
239+
->willReturnCallback(static function ($classOrObject) use ($formMetadata, $authorMetadata) {
240+
if (Author::class === $classOrObject || $classOrObject instanceof Author) {
241+
return $authorMetadata;
242+
}
243+
244+
if (Form::class === $classOrObject || $classOrObject instanceof Form) {
245+
return $formMetadata;
246+
}
247+
248+
return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class);
249+
})
250+
;
251+
252+
$validator = Validation::createValidatorBuilder()
253+
->setMetadataFactory($metadataFactory)
254+
->getValidator()
255+
;
256+
257+
$form = Forms::createFormFactoryBuilder()
258+
->addExtension(new ValidatorExtension($validator))
259+
->getFormFactory()
260+
->create(FormTypeTest::TESTED_TYPE)
261+
->add('firstName', TextTypeTest::TESTED_TYPE, [
262+
'constraints_from_entity' => Author::class,
263+
'constraints_from_property' => $propertyName ?? null,
264+
])
265+
;
266+
267+
return $form;
268+
}
269+
157270
protected function createForm(array $options = [])
158271
{
159272
return $this->factory->create(FormTypeTest::TESTED_TYPE, null, $options);

0 commit comments

Comments
 (0)