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:
Fabien Potencier 2020-09-01 09:49:57 +02:00
commit ccfc4ba269
6 changed files with 221 additions and 12 deletions

View File

@ -126,7 +126,12 @@ return static function (ContainerConfigurator $container) {
->args([service('request_stack')])
->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])
->set('form.type_extension.repeated.validator', RepeatedTypeValidatorExtension::class)

View File

@ -4,7 +4,8 @@ CHANGELOG
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
-----

View File

@ -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\ViolationMapper\ViolationMapper;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormRendererInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
@ -28,10 +30,10 @@ class FormTypeValidatorExtension extends BaseValidatorExtension
private $violationMapper;
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->violationMapper = new ViolationMapper();
$this->violationMapper = new ViolationMapper($formRenderer, $translator);
$this->legacyErrorMessages = $legacyErrorMessages;
}

View File

@ -13,9 +13,11 @@ namespace Symfony\Component\Form\Extension\Validator;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Form\FormRendererInterface;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Extension supporting the Symfony Validator component in forms.
@ -25,9 +27,11 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
class ValidatorExtension extends AbstractExtension
{
private $validator;
private $formRenderer;
private $translator;
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;
@ -43,6 +47,8 @@ class ValidatorExtension extends AbstractExtension
$metadata->addConstraint(new Traverse(false));
$this->validator = $validator;
$this->formRenderer = $formRenderer;
$this->translator = $translator;
}
public function loadTypeGuesser()
@ -53,7 +59,7 @@ class ValidatorExtension extends AbstractExtension
protected function loadTypeExtensions()
{
return [
new Type\FormTypeValidatorExtension($this->validator, $this->legacyErrorMessages),
new Type\FormTypeValidatorExtension($this->validator, $this->legacyErrorMessages, $this->formRenderer, $this->translator),
new Type\RepeatedTypeValidatorExtension(),
new Type\SubmitTypeValidatorExtension(),
];

View File

@ -13,21 +13,28 @@ namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormRendererInterface;
use Symfony\Component\Form\Util\InheritDataAwareIterator;
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
use Symfony\Component\PropertyAccess\PropertyPathIterator;
use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ViolationMapper implements ViolationMapperInterface
{
/**
* @var bool
*/
private $allowNonSynchronized;
private $formRenderer;
private $translator;
private $allowNonSynchronized = false;
public function __construct(FormRendererInterface $formRenderer = null, TranslatorInterface $translator = null)
{
$this->formRenderer = $formRenderer;
$this->translator = $translator;
}
/**
* {@inheritdoc}
@ -124,9 +131,49 @@ class ViolationMapper implements ViolationMapperInterface
// Only add the error if the form is synchronized
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(
$violation->getMessage(),
$violation->getMessageTemplate(),
$message,
$messageTemplate,
$violation->getParameters(),
$violation->getPlural(),
$violation

View File

@ -22,10 +22,12 @@ use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigBuilder;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormRenderer;
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;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @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($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());
}
}
}