[Form] TransformationFailedException: Support specifying message to display

This commit is contained in:
Maxime Steinhausser 2016-12-18 14:08:54 +01:00
parent 76260e7d44
commit d11055cc1c
5 changed files with 169 additions and 7 deletions

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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()
);
}
}

View File

@ -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()
{