From d11055cc1cfb6b4403eb1bbc2a61c01893abaf99 Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Sun, 18 Dec 2016 14:08:54 +0100 Subject: [PATCH] [Form] TransformationFailedException: Support specifying message to display --- .../TransformationFailedException.php | 31 +++++++ .../Validator/Constraints/FormValidator.php | 12 ++- src/Symfony/Component/Form/Form.php | 8 +- .../Extension/Core/Type/FormTypeTest.php | 84 +++++++++++++++++++ .../Constraints/FormValidatorTest.php | 41 +++++++++ 5 files changed, 169 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Form/Exception/TransformationFailedException.php b/src/Symfony/Component/Form/Exception/TransformationFailedException.php index d32896e69c91..89eba088edbd 100644 --- a/src/Symfony/Component/Form/Exception/TransformationFailedException.php +++ b/src/Symfony/Component/Form/Exception/TransformationFailedException.php @@ -18,4 +18,35 @@ */ class TransformationFailedException extends RuntimeException { + private $invalidMessage; + private $invalidMessageParameters; + + public function __construct(string $message = '', int $code = 0, \Throwable $previous = null, string $invalidMessage = null, array $invalidMessageParameters = []) + { + parent::__construct($message, $code, $previous); + + $this->setInvalidMessage($invalidMessage, $invalidMessageParameters); + } + + /** + * Sets the message that will be shown to the user. + * + * @param string|null $invalidMessage The message or message key + * @param array $invalidMessageParameters Data to be passed into the translator + */ + public function setInvalidMessage(string $invalidMessage = null, array $invalidMessageParameters = []): void + { + $this->invalidMessage = $invalidMessage; + $this->invalidMessageParameters = $invalidMessageParameters; + } + + public function getInvalidMessage(): ?string + { + return $this->invalidMessage; + } + + public function getInvalidMessageParameters(): array + { + return $this->invalidMessageParameters; + } } diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php index 8a5cd14ff4dc..ca3cf80fde35 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -118,12 +118,18 @@ public function validate($form, Constraint $formConstraint) ? (string) $form->getViewData() : \gettype($form->getViewData()); + $failure = $form->getTransformationFailure(); + $this->context->setConstraint($formConstraint); - $this->context->buildViolation($config->getOption('invalid_message')) - ->setParameters(array_replace(['{{ value }}' => $clientDataAsString], $config->getOption('invalid_message_parameters'))) + $this->context->buildViolation($failure->getInvalidMessage() ?? $config->getOption('invalid_message')) + ->setParameters(array_replace( + ['{{ value }}' => $clientDataAsString], + $config->getOption('invalid_message_parameters'), + $failure->getInvalidMessageParameters() + )) ->setInvalidValue($form->getViewData()) ->setCode(Form::NOT_SYNCHRONIZED_ERROR) - ->setCause($form->getTransformationFailure()) + ->setCause($failure) ->addViolation(); } } diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 40b7f4d23db6..30bd02cc160b 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -1070,7 +1070,7 @@ private function modelToNorm($value) $value = $transformer->transform($value); } } catch (TransformationFailedException $exception) { - throw new TransformationFailedException('Unable to transform value for property path "'.$this->getPropertyPath().'": '.$exception->getMessage(), $exception->getCode(), $exception); + throw new TransformationFailedException('Unable to transform value for property path "'.$this->getPropertyPath().'": '.$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); } return $value; @@ -1094,7 +1094,7 @@ private function normToModel($value) $value = $transformers[$i]->reverseTransform($value); } } catch (TransformationFailedException $exception) { - throw new TransformationFailedException('Unable to reverse value for property path "'.$this->getPropertyPath().'": '.$exception->getMessage(), $exception->getCode(), $exception); + throw new TransformationFailedException('Unable to reverse value for property path "'.$this->getPropertyPath().'": '.$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); } return $value; @@ -1125,7 +1125,7 @@ private function normToView($value) $value = $transformer->transform($value); } } catch (TransformationFailedException $exception) { - throw new TransformationFailedException('Unable to transform value for property path "'.$this->getPropertyPath().'": '.$exception->getMessage(), $exception->getCode(), $exception); + throw new TransformationFailedException('Unable to transform value for property path "'.$this->getPropertyPath().'": '.$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); } return $value; @@ -1153,7 +1153,7 @@ private function viewToNorm($value) $value = $transformers[$i]->reverseTransform($value); } } catch (TransformationFailedException $exception) { - throw new TransformationFailedException('Unable to reverse value for property path "'.$this->getPropertyPath().'": '.$exception->getMessage(), $exception->getCode(), $exception); + throw new TransformationFailedException('Unable to reverse value for property path "'.$this->getPropertyPath().'": '.$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); } return $value; diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php index 0c7f97dc1e74..2607f1d3760a 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php @@ -12,10 +12,18 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\Type\CurrencyType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Validator\ValidatorExtension; use Symfony\Component\Form\FormError; +use Symfony\Component\Form\Forms; use Symfony\Component\Form\Tests\Fixtures\Author; use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer; use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\Validator\Validation; class FormTest_AuthorWithoutRefSetter { @@ -624,6 +632,32 @@ public function testNormDataIsPassedToView() $this->assertSame('baz', $view->vars['value']); } + public function testDataMapperTransformationFailedExceptionInvalidMessageIsUsed() + { + $money = new Money(20.5, 'EUR'); + $factory = Forms::createFormFactoryBuilder() + ->addExtensions([new ValidatorExtension(Validation::createValidator())]) + ->getFormFactory() + ; + + $builder = $factory + ->createBuilder(FormType::class, $money, ['invalid_message' => 'not the one to display']) + ->add('amount', TextType::class) + ->add('currency', CurrencyType::class) + ; + $builder->setDataMapper(new MoneyDataMapper()); + $form = $builder->getForm(); + + $form->submit(['amount' => 'invalid_amount', 'currency' => 'USD']); + + $this->assertFalse($form->isValid()); + $this->assertNull($form->getData()); + $this->assertCount(1, $form->getErrors()); + $this->assertSame('Expected numeric value', $form->getTransformationFailure()->getMessage()); + $error = $form->getErrors()[0]; + $this->assertSame('Money amount should be numeric. "invalid_amount" is invalid.', $error->getMessage()); + } + // https://github.com/symfony/symfony/issues/6862 public function testPassZeroLabelToView() { @@ -700,3 +734,53 @@ public function testPreferOwnHelpTranslationParameters() $this->assertEquals(['%parent_param%' => 'parent_value', '%override_param%' => 'child_value'], $view['child']->vars['help_translation_parameters']); } } + +class Money +{ + private $amount; + private $currency; + + public function __construct($amount, $currency) + { + $this->amount = $amount; + $this->currency = $currency; + } + + public function getAmount() + { + return $this->amount; + } + + public function getCurrency() + { + return $this->currency; + } +} + +class MoneyDataMapper implements DataMapperInterface +{ + public function mapDataToForms($data, $forms) + { + $forms = iterator_to_array($forms); + $forms['amount']->setData($data ? $data->getAmount() : 0); + $forms['currency']->setData($data ? $data->getCurrency() : 'EUR'); + } + + public function mapFormsToData($forms, &$data) + { + $forms = iterator_to_array($forms); + + $amount = $forms['amount']->getData(); + if (!is_numeric($amount)) { + $failure = new TransformationFailedException('Expected numeric value'); + $failure->setInvalidMessage('Money amount should be numeric. {{ amount }} is invalid.', ['{{ amount }}' => json_encode($amount)]); + + throw $failure; + } + + $data = new Money( + $forms['amount']->getData(), + $forms['currency']->getData() + ); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php index f23cc204f81a..45fe0ebd8be7 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -343,6 +343,47 @@ function () { throw new TransformationFailedException(); } ->assertRaised(); } + public function testTransformationFailedExceptionInvalidMessageIsUsed() + { + $object = $this->createMock('\stdClass'); + + $form = $this + ->getBuilder('name', '\stdClass', [ + 'invalid_message' => 'invalid_message_key', + 'invalid_message_parameters' => ['{{ foo }}' => 'foo'], + ]) + ->setData($object) + ->addViewTransformer(new CallbackTransformer( + function ($data) { return $data; }, + function () { + $failure = new TransformationFailedException(); + $failure->setInvalidMessage('safe message to be used', ['{{ bar }}' => 'bar']); + + throw $failure; + } + )) + ->getForm() + ; + + $form->submit('value'); + + $this->expectNoValidate(); + + $this->validator->validate($form, new Form()); + + $this->buildViolation('safe message to be used') + ->setParameters([ + '{{ value }}' => 'value', + '{{ foo }}' => 'foo', + '{{ bar }}' => 'bar', + ]) + ->setInvalidValue('value') + ->setCode(Form::NOT_SYNCHRONIZED_ERROR) + ->setCause($form->getTransformationFailure()) + ->assertRaised() + ; + } + // https://github.com/symfony/symfony/issues/4359 public function testDontMarkInvalidIfAnyChildIsNotSynchronized() {