diff --git a/src/Symfony/Component/Form/Exception/TransformationFailedException.php b/src/Symfony/Component/Form/Exception/TransformationFailedException.php index d32896e69c911..89eba088edbdb 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 8a5cd14ff4dca..ca3cf80fde358 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 40b7f4d23db69..30bd02cc160ba 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 0c7f97dc1e744..2607f1d3760ab 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 f23cc204f81a3..45fe0ebd8be7e 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() {
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: