feature #20978 [Form] TransformationFailedException: Support specifying message to display (ogizanagi)
This PR was merged into the 4.3-dev branch.
Discussion
----------
[Form] TransformationFailedException: Support specifying message to display
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | #22501
| License | MIT
| Doc PR | N/A
Currently, a failed transformation can't display a very accurate message, as it only uses the value of the `invalid_message` option. I suggest to add more flexibility, somehow the same way we did for `CustomUserMessageAuthenticationException`.
A test case in `FormTypeTest` shows a use-case based on @webmozart's [blog post about value/immutable objects in Symfony forms](https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/).
~I named the exception properties and methods the same way as for `CustomUserMessageAuthenticationException`, but do you think the followings would be better:~
- ~`setSafeMessage` ➜ `setInvalidMessage`~
- ~`getMessageKey` ➜ `getInvalidMessageKey`~
- ~`getMessageData` ➜ `getInvalidMessageParameters`~
~in order to echoes `invalid_message` and `invalid_message_parameters` options?~
=> Replaced to use `invalidMessage` & `invalidMessageParameters`
Commits
-------
d11055cc1c
[Form] TransformationFailedException: Support specifying message to display
This commit is contained in:
commit
79aeed640a
@ -18,4 +18,35 @@ namespace Symfony\Component\Form\Exception;
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -118,12 +118,18 @@ class FormValidator extends ConstraintValidator
|
||||
? (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();
|
||||
}
|
||||
}
|
||||
|
@ -1070,7 +1070,7 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
||||
$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 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
||||
$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 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
||||
$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 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
|
||||
$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;
|
||||
|
@ -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 @@ class FormTypeTest extends BaseTypeTest
|
||||
$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 @@ class FormTypeTest extends BaseTypeTest
|
||||
$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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -343,6 +343,47 @@ class FormValidatorTest extends ConstraintValidatorTestCase
|
||||
->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()
|
||||
{
|
||||
|
Reference in New Issue
Block a user