feature #35338 Added support for using the "{{ label }}" placeholder in constraint messages (a-menshchikov)
This PR was squashed before being merged into the 5.2-dev branch.
Discussion
----------
Added support for using the "{{ label }}" placeholder in constraint messages
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Tickets | #12238
| License | MIT
| Doc PR |
- [ ] Add docs PR
Commits
-------
0d9f44235c
Added support for using the "{{ label }}" placeholder in constraint messages
This commit is contained in:
commit
ccfc4ba269
@ -126,7 +126,12 @@ return static function (ContainerConfigurator $container) {
|
|||||||
->args([service('request_stack')])
|
->args([service('request_stack')])
|
||||||
|
|
||||||
->set('form.type_extension.form.validator', FormTypeValidatorExtension::class)
|
->set('form.type_extension.form.validator', FormTypeValidatorExtension::class)
|
||||||
->args([service('validator')])
|
->args([
|
||||||
|
service('validator'),
|
||||||
|
true,
|
||||||
|
service('twig.form.renderer')->ignoreOnInvalid(),
|
||||||
|
service('translator')->ignoreOnInvalid(),
|
||||||
|
])
|
||||||
->tag('form.type_extension', ['extended-type' => FormType::class])
|
->tag('form.type_extension', ['extended-type' => FormType::class])
|
||||||
|
|
||||||
->set('form.type_extension.repeated.validator', RepeatedTypeValidatorExtension::class)
|
->set('form.type_extension.repeated.validator', RepeatedTypeValidatorExtension::class)
|
||||||
|
@ -4,7 +4,8 @@ CHANGELOG
|
|||||||
5.2.0
|
5.2.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
* added `FormErrorNormalizer`
|
* added `FormErrorNormalizer`
|
||||||
|
* Added support for using the `{{ label }}` placeholder in constraint messages, which is replaced in the `ViolationMapper` by the corresponding field form label.
|
||||||
|
|
||||||
5.1.0
|
5.1.0
|
||||||
-----
|
-----
|
||||||
|
@ -15,9 +15,11 @@ use Symfony\Component\Form\Extension\Core\Type\FormType;
|
|||||||
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
|
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
|
||||||
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
|
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormRendererInterface;
|
||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
@ -28,10 +30,10 @@ class FormTypeValidatorExtension extends BaseValidatorExtension
|
|||||||
private $violationMapper;
|
private $violationMapper;
|
||||||
private $legacyErrorMessages;
|
private $legacyErrorMessages;
|
||||||
|
|
||||||
public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true)
|
public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, FormRendererInterface $formRenderer = null, TranslatorInterface $translator = null)
|
||||||
{
|
{
|
||||||
$this->validator = $validator;
|
$this->validator = $validator;
|
||||||
$this->violationMapper = new ViolationMapper();
|
$this->violationMapper = new ViolationMapper($formRenderer, $translator);
|
||||||
$this->legacyErrorMessages = $legacyErrorMessages;
|
$this->legacyErrorMessages = $legacyErrorMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,9 +13,11 @@ namespace Symfony\Component\Form\Extension\Validator;
|
|||||||
|
|
||||||
use Symfony\Component\Form\AbstractExtension;
|
use Symfony\Component\Form\AbstractExtension;
|
||||||
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
|
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
|
||||||
|
use Symfony\Component\Form\FormRendererInterface;
|
||||||
use Symfony\Component\Validator\Constraints\Traverse;
|
use Symfony\Component\Validator\Constraints\Traverse;
|
||||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension supporting the Symfony Validator component in forms.
|
* Extension supporting the Symfony Validator component in forms.
|
||||||
@ -25,9 +27,11 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
|||||||
class ValidatorExtension extends AbstractExtension
|
class ValidatorExtension extends AbstractExtension
|
||||||
{
|
{
|
||||||
private $validator;
|
private $validator;
|
||||||
|
private $formRenderer;
|
||||||
|
private $translator;
|
||||||
private $legacyErrorMessages;
|
private $legacyErrorMessages;
|
||||||
|
|
||||||
public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true)
|
public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, FormRendererInterface $formRenderer = null, TranslatorInterface $translator = null)
|
||||||
{
|
{
|
||||||
$this->legacyErrorMessages = $legacyErrorMessages;
|
$this->legacyErrorMessages = $legacyErrorMessages;
|
||||||
|
|
||||||
@ -43,6 +47,8 @@ class ValidatorExtension extends AbstractExtension
|
|||||||
$metadata->addConstraint(new Traverse(false));
|
$metadata->addConstraint(new Traverse(false));
|
||||||
|
|
||||||
$this->validator = $validator;
|
$this->validator = $validator;
|
||||||
|
$this->formRenderer = $formRenderer;
|
||||||
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadTypeGuesser()
|
public function loadTypeGuesser()
|
||||||
@ -53,7 +59,7 @@ class ValidatorExtension extends AbstractExtension
|
|||||||
protected function loadTypeExtensions()
|
protected function loadTypeExtensions()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new Type\FormTypeValidatorExtension($this->validator, $this->legacyErrorMessages),
|
new Type\FormTypeValidatorExtension($this->validator, $this->legacyErrorMessages, $this->formRenderer, $this->translator),
|
||||||
new Type\RepeatedTypeValidatorExtension(),
|
new Type\RepeatedTypeValidatorExtension(),
|
||||||
new Type\SubmitTypeValidatorExtension(),
|
new Type\SubmitTypeValidatorExtension(),
|
||||||
];
|
];
|
||||||
|
@ -13,21 +13,28 @@ namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
|
|||||||
|
|
||||||
use Symfony\Component\Form\FormError;
|
use Symfony\Component\Form\FormError;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\Form\FormRendererInterface;
|
||||||
use Symfony\Component\Form\Util\InheritDataAwareIterator;
|
use Symfony\Component\Form\Util\InheritDataAwareIterator;
|
||||||
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
|
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
|
||||||
use Symfony\Component\PropertyAccess\PropertyPathIterator;
|
use Symfony\Component\PropertyAccess\PropertyPathIterator;
|
||||||
use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
|
use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
|
||||||
use Symfony\Component\Validator\ConstraintViolation;
|
use Symfony\Component\Validator\ConstraintViolation;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
*/
|
*/
|
||||||
class ViolationMapper implements ViolationMapperInterface
|
class ViolationMapper implements ViolationMapperInterface
|
||||||
{
|
{
|
||||||
/**
|
private $formRenderer;
|
||||||
* @var bool
|
private $translator;
|
||||||
*/
|
private $allowNonSynchronized = false;
|
||||||
private $allowNonSynchronized;
|
|
||||||
|
public function __construct(FormRendererInterface $formRenderer = null, TranslatorInterface $translator = null)
|
||||||
|
{
|
||||||
|
$this->formRenderer = $formRenderer;
|
||||||
|
$this->translator = $translator;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
@ -124,9 +131,49 @@ class ViolationMapper implements ViolationMapperInterface
|
|||||||
|
|
||||||
// Only add the error if the form is synchronized
|
// Only add the error if the form is synchronized
|
||||||
if ($this->acceptsErrors($scope)) {
|
if ($this->acceptsErrors($scope)) {
|
||||||
|
$labelFormat = $scope->getConfig()->getOption('label_format');
|
||||||
|
|
||||||
|
if (null !== $labelFormat) {
|
||||||
|
$label = str_replace(
|
||||||
|
[
|
||||||
|
'%name%',
|
||||||
|
'%id%',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
$scope->getName(),
|
||||||
|
(string) $scope->getPropertyPath(),
|
||||||
|
],
|
||||||
|
$labelFormat
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$label = $scope->getConfig()->getOption('label');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $label && null !== $this->formRenderer) {
|
||||||
|
$label = $this->formRenderer->humanize($scope->getName());
|
||||||
|
} elseif (null === $label) {
|
||||||
|
$label = $scope->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false !== $label && null !== $this->translator) {
|
||||||
|
$label = $this->translator->trans(
|
||||||
|
$label,
|
||||||
|
$scope->getConfig()->getOption('label_translation_parameters', []),
|
||||||
|
$scope->getConfig()->getOption('translation_domain')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = $violation->getMessage();
|
||||||
|
$messageTemplate = $violation->getMessageTemplate();
|
||||||
|
|
||||||
|
if (false !== $label) {
|
||||||
|
$message = str_replace('{{ label }}', $label, $message);
|
||||||
|
$messageTemplate = str_replace('{{ label }}', $label, $messageTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
$scope->addError(new FormError(
|
$scope->addError(new FormError(
|
||||||
$violation->getMessage(),
|
$message,
|
||||||
$violation->getMessageTemplate(),
|
$messageTemplate,
|
||||||
$violation->getParameters(),
|
$violation->getParameters(),
|
||||||
$violation->getPlural(),
|
$violation->getPlural(),
|
||||||
$violation
|
$violation
|
||||||
|
@ -22,10 +22,12 @@ 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\FormRenderer;
|
||||||
use Symfony\Component\Form\Tests\Extension\Validator\ViolationMapper\Fixtures\Issue;
|
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;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
@ -1590,4 +1592,150 @@ class ViolationMapperTest extends TestCase
|
|||||||
$this->assertEquals([$this->getFormError($violation2, $grandChild2)], iterator_to_array($grandChild2->getErrors()), $grandChild2->getName().' should have an error, but has none');
|
$this->assertEquals([$this->getFormError($violation2, $grandChild2)], iterator_to_array($grandChild2->getErrors()), $grandChild2->getName().' should have an error, but has none');
|
||||||
$this->assertEquals([$this->getFormError($violation3, $grandChild3)], iterator_to_array($grandChild3->getErrors()), $grandChild3->getName().' should have an error, but has none');
|
$this->assertEquals([$this->getFormError($violation3, $grandChild3)], iterator_to_array($grandChild3->getErrors()), $grandChild3->getName().' should have an error, but has none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMessageWithLabel1()
|
||||||
|
{
|
||||||
|
$renderer = $this->getMockBuilder(FormRenderer::class)
|
||||||
|
->setMethods(null)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock()
|
||||||
|
;
|
||||||
|
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||||
|
$translator->expects($this->any())->method('trans')->willReturnMap([
|
||||||
|
['Name', [], null, null, 'Custom Name'],
|
||||||
|
]);
|
||||||
|
$this->mapper = new ViolationMapper($renderer, $translator);
|
||||||
|
|
||||||
|
$parent = $this->getForm('parent');
|
||||||
|
$child = $this->getForm('name', 'name');
|
||||||
|
$parent->add($child);
|
||||||
|
|
||||||
|
$parent->submit([]);
|
||||||
|
|
||||||
|
$violation = new ConstraintViolation('Message {{ label }}', null, [], null, 'data.name', null);
|
||||||
|
$this->mapper->mapViolation($violation, $parent);
|
||||||
|
|
||||||
|
$this->assertCount(1, $child->getErrors(), $child->getName().' should have an error, but has none');
|
||||||
|
|
||||||
|
$errors = iterator_to_array($child->getErrors());
|
||||||
|
if (isset($errors[0])) {
|
||||||
|
/** @var FormError $error */
|
||||||
|
$error = $errors[0];
|
||||||
|
$this->assertSame('Message Custom Name', $error->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMessageWithLabel2()
|
||||||
|
{
|
||||||
|
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||||
|
$translator->expects($this->any())->method('trans')->willReturnMap([
|
||||||
|
['options_label', [], null, null, 'Translated Label'],
|
||||||
|
]);
|
||||||
|
$this->mapper = new ViolationMapper(null, $translator);
|
||||||
|
|
||||||
|
$parent = $this->getForm('parent');
|
||||||
|
|
||||||
|
$config = new FormConfigBuilder('name', null, $this->dispatcher, [
|
||||||
|
'error_mapping' => [],
|
||||||
|
'label' => 'options_label',
|
||||||
|
]);
|
||||||
|
$config->setMapped(true);
|
||||||
|
$config->setInheritData(false);
|
||||||
|
$config->setPropertyPath('name');
|
||||||
|
$config->setCompound(true);
|
||||||
|
$config->setDataMapper(new PropertyPathMapper());
|
||||||
|
|
||||||
|
$child = new Form($config);
|
||||||
|
$parent->add($child);
|
||||||
|
|
||||||
|
$parent->submit([]);
|
||||||
|
|
||||||
|
$violation = new ConstraintViolation('Message {{ label }}', null, [], null, 'data.name', null);
|
||||||
|
$this->mapper->mapViolation($violation, $parent);
|
||||||
|
|
||||||
|
$this->assertCount(1, $child->getErrors(), $child->getName().' should have an error, but has none');
|
||||||
|
|
||||||
|
$errors = iterator_to_array($child->getErrors());
|
||||||
|
if (isset($errors[0])) {
|
||||||
|
/** @var FormError $error */
|
||||||
|
$error = $errors[0];
|
||||||
|
$this->assertSame('Message Translated Label', $error->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMessageWithLabelFormat1()
|
||||||
|
{
|
||||||
|
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||||
|
$translator->expects($this->any())->method('trans')->willReturnMap([
|
||||||
|
['form.custom', [], null, null, 'Translated 1st Custom Label'],
|
||||||
|
]);
|
||||||
|
$this->mapper = new ViolationMapper(null, $translator);
|
||||||
|
|
||||||
|
$parent = $this->getForm('parent');
|
||||||
|
|
||||||
|
$config = new FormConfigBuilder('custom', null, $this->dispatcher, [
|
||||||
|
'error_mapping' => [],
|
||||||
|
'label_format' => 'form.%name%',
|
||||||
|
]);
|
||||||
|
$config->setMapped(true);
|
||||||
|
$config->setInheritData(false);
|
||||||
|
$config->setPropertyPath('custom');
|
||||||
|
$config->setCompound(true);
|
||||||
|
$config->setDataMapper(new PropertyPathMapper());
|
||||||
|
|
||||||
|
$child = new Form($config);
|
||||||
|
$parent->add($child);
|
||||||
|
|
||||||
|
$parent->submit([]);
|
||||||
|
|
||||||
|
$violation = new ConstraintViolation('Message {{ label }}', null, [], null, 'data.custom', null);
|
||||||
|
$this->mapper->mapViolation($violation, $parent);
|
||||||
|
|
||||||
|
$this->assertCount(1, $child->getErrors(), $child->getName().' should have an error, but has none');
|
||||||
|
|
||||||
|
$errors = iterator_to_array($child->getErrors());
|
||||||
|
if (isset($errors[0])) {
|
||||||
|
/** @var FormError $error */
|
||||||
|
$error = $errors[0];
|
||||||
|
$this->assertSame('Message Translated 1st Custom Label', $error->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMessageWithLabelFormat2()
|
||||||
|
{
|
||||||
|
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||||
|
$translator->expects($this->any())->method('trans')->willReturnMap([
|
||||||
|
['form_custom-id', [], null, null, 'Translated 2nd Custom Label'],
|
||||||
|
]);
|
||||||
|
$this->mapper = new ViolationMapper(null, $translator);
|
||||||
|
|
||||||
|
$parent = $this->getForm('parent');
|
||||||
|
|
||||||
|
$config = new FormConfigBuilder('custom-id', null, $this->dispatcher, [
|
||||||
|
'error_mapping' => [],
|
||||||
|
'label_format' => 'form_%id%',
|
||||||
|
]);
|
||||||
|
$config->setMapped(true);
|
||||||
|
$config->setInheritData(false);
|
||||||
|
$config->setPropertyPath('custom-id');
|
||||||
|
$config->setCompound(true);
|
||||||
|
$config->setDataMapper(new PropertyPathMapper());
|
||||||
|
|
||||||
|
$child = new Form($config);
|
||||||
|
$parent->add($child);
|
||||||
|
|
||||||
|
$parent->submit([]);
|
||||||
|
|
||||||
|
$violation = new ConstraintViolation('Message {{ label }}', null, [], null, 'data.custom-id', null);
|
||||||
|
$this->mapper->mapViolation($violation, $parent);
|
||||||
|
|
||||||
|
$this->assertCount(1, $child->getErrors(), $child->getName().' should have an error, but has none');
|
||||||
|
|
||||||
|
$errors = iterator_to_array($child->getErrors());
|
||||||
|
if (isset($errors[0])) {
|
||||||
|
/** @var FormError $error */
|
||||||
|
$error = $errors[0];
|
||||||
|
$this->assertSame('Message Translated 2nd Custom Label', $error->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user