diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index 6d6d07d240ab..736f718ccf13 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -473,14 +473,28 @@ {% endif %} - {% if error.cause is empty %} - Unknown. - {% elseif error.cause.root is defined %} - Constraint Violation
-
{{ error.cause.root }}{% if error.cause.path is not empty %}{% if error.cause.path|first != '[' %}.{% endif %}{{ error.cause.path }}{% endif %} = {{ error.cause.value }}
+ {% for trace in error.trace %} + {% if not loop.first %} +
Caused by:

+ {% endif %} + {% if trace.root is defined %} + {{ trace.class }}
+
+                                    {{- trace.root -}}
+                                    {%- if trace.path is not empty -%}
+                                        {%- if trace.path|first != '[' %}.{% endif -%}
+                                        {{- trace.path -}}
+                                    {%- endif %} = {{ trace.value -}}
+                                
+ {% elseif trace.message is defined %} + {{ trace.class }}
+
{{ trace.message }}
+ {% else %} +
{{ trace }}
+ {% endif %} {% else %} -
{{ error.cause }}
- {% endif %} + Unknown. + {% endfor %} {% endfor %} diff --git a/src/Symfony/Component/Form/Button.php b/src/Symfony/Component/Form/Button.php index 380b20f4ddc2..0ba4c8138cf3 100644 --- a/src/Symfony/Component/Form/Button.php +++ b/src/Symfony/Component/Form/Button.php @@ -344,6 +344,15 @@ public function isSynchronized() return true; } + /** + * Unsupported method. + * + * @return null Always returns null + */ + public function getTransformationFailure() + { + } + /** * Unsupported method. * diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 4381726126d4..dfc2e89ea7cd 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -21,6 +21,7 @@ CHANGELOG * ObjectChoiceList now compares choices by their value, if a value path is given * you can now pass interface names in the "data_class" option + * [BC BREAK] added `FormInterface::getTransformationFailure()` 2.4.0 ----- diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php index 80e910177a89..0ae5560afeab 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php @@ -115,18 +115,39 @@ public function extractSubmittedData(FormInterface $form) 'origin' => is_object($error->getOrigin()) ? spl_object_hash($error->getOrigin()) : null, + 'trace' => array(), ); $cause = $error->getCause(); - if ($cause instanceof ConstraintViolationInterface) { - $errorData['cause'] = array( - 'root' => $this->valueExporter->exportValue($cause->getRoot()), - 'path' => $this->valueExporter->exportValue($cause->getPropertyPath()), - 'value' => $this->valueExporter->exportValue($cause->getInvalidValue()), - ); - } else { - $errorData['cause'] = null !== $cause ? $this->valueExporter->exportValue($cause) : null; + while (null !== $cause) { + if ($cause instanceof ConstraintViolationInterface) { + $errorData['trace'][] = array( + 'class' => $this->valueExporter->exportValue(get_class($cause)), + 'root' => $this->valueExporter->exportValue($cause->getRoot()), + 'path' => $this->valueExporter->exportValue($cause->getPropertyPath()), + 'value' => $this->valueExporter->exportValue($cause->getInvalidValue()), + ); + + $cause = method_exists($cause, 'getCause') ? $cause->getCause() : null; + + continue; + } + + if ($cause instanceof \Exception) { + $errorData['trace'][] = array( + 'class' => $this->valueExporter->exportValue(get_class($cause)), + 'message' => $this->valueExporter->exportValue($cause->getMessage()), + ); + + $cause = $cause->getPrevious(); + + continue; + } + + $errorData['trace'][] = $cause; + + break; } $data['errors'][] = $errorData; diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php index 1fe114705107..7c0dc44ab180 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -103,6 +103,7 @@ public function validate($form, Constraint $constraint) ->setParameters(array_replace(array('{{ value }}' => $clientDataAsString), $config->getOption('invalid_message_parameters'))) ->setInvalidValue($form->getViewData()) ->setCode(Form::ERR_INVALID) + ->setCause($form->getTransformationFailure()) ->addViolation(); } } diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 90048d3fb558..9b259e820b13 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -121,12 +121,10 @@ class Form implements \IteratorAggregate, FormInterface private $extraData = array(); /** - * Whether the data in model, normalized and view format is - * synchronized. Data may not be synchronized if transformation errors - * occur. - * @var bool + * Returns the transformation failure generated during submission, if any + * @var TransformationFailedException|null */ - private $synchronized = true; + private $transformationFailure; /** * Whether the form's data has been initialized. @@ -634,7 +632,7 @@ public function submit($submittedData, $clearMissing = true) $viewData = $this->normToView($normData); } } catch (TransformationFailedException $e) { - $this->synchronized = false; + $this->transformationFailure = $e; // If $viewData was not yet set, set it to $submittedData so that // the erroneous data is accessible on the form. @@ -711,7 +709,15 @@ public function isBound() */ public function isSynchronized() { - return $this->synchronized; + return null === $this->transformationFailure; + } + + /** + * {@inheritdoc} + */ + public function getTransformationFailure() + { + return $this->transformationFailure; } /** diff --git a/src/Symfony/Component/Form/FormInterface.php b/src/Symfony/Component/Form/FormInterface.php index 274ce04e620a..1b9e6e47775d 100644 --- a/src/Symfony/Component/Form/FormInterface.php +++ b/src/Symfony/Component/Form/FormInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Form; +use Symfony\Component\Form\Exception\TransformationFailedException; + /** * A form group bundling multiple forms in a hierarchical structure. * @@ -230,10 +232,20 @@ public function isEmpty(); /** * Returns whether the data in the different formats is synchronized. * + * If the data is not synchronized, you can get the transformation failure + * by calling {@link getTransformationFailure()}. + * * @return bool */ public function isSynchronized(); + /** + * Returns the data transformation failure, if any. + * + * @return TransformationFailedException|null The transformation failure + */ + public function getTransformationFailure(); + /** * Initializes the form tree. * diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php index 77b2d4472a06..deb6069ee2f5 100644 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php @@ -319,7 +319,7 @@ public function testExtractSubmittedDataStoresErrors() 'norm' => "'Foobar'", ), 'errors' => array( - array('message' => 'Invalid!', 'origin' => null, 'cause' => null), + array('message' => 'Invalid!', 'origin' => null, 'trace' => array()), ), 'synchronized' => 'true', ), $this->dataExtractor->extractSubmittedData($form)); @@ -340,7 +340,7 @@ public function testExtractSubmittedDataStoresErrorOrigin() 'norm' => "'Foobar'", ), 'errors' => array( - array('message' => 'Invalid!', 'origin' => spl_object_hash($form), 'cause' => null), + array('message' => 'Invalid!', 'origin' => spl_object_hash($form), 'trace' => array()), ), 'synchronized' => 'true', ), $this->dataExtractor->extractSubmittedData($form)); @@ -360,7 +360,12 @@ public function testExtractSubmittedDataStoresErrorCause() 'norm' => "'Foobar'", ), 'errors' => array( - array('message' => 'Invalid!', 'origin' => null, 'cause' => 'object(Exception)'), + array('message' => 'Invalid!', 'origin' => null, 'trace' => array( + array( + 'class' => "'Exception'", + 'message' => "''", + ), + )), ), 'synchronized' => 'true', ), $this->dataExtractor->extractSubmittedData($form)); 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 99c5e584c1ae..877797f16389 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -225,11 +225,14 @@ function () { throw new TransformationFailedException(); } $this->validator->validate($form, new Form()); + $is2Dot4Api = Validation::API_VERSION_2_4 === $this->getApiVersion(); + $this->buildViolation('invalid_message_key') ->setParameter('{{ value }}', 'foo') ->setParameter('{{ foo }}', 'bar') ->setInvalidValue('foo') ->setCode(Form::ERR_INVALID) + ->setCause($is2Dot4Api ? null : $form->getTransformationFailure()) ->assertRaised(); } @@ -259,11 +262,14 @@ function () { throw new TransformationFailedException(); } $this->validator->validate($form, new Form()); + $is2Dot4Api = Validation::API_VERSION_2_4 === $this->getApiVersion(); + $this->buildViolation('invalid_message_key') ->setParameter('{{ value }}', 'foo') ->setParameter('{{ foo }}', 'bar') ->setInvalidValue('foo') ->setCode(Form::ERR_INVALID) + ->setCause($is2Dot4Api ? null : $form->getTransformationFailure()) ->assertRaised(); } @@ -293,10 +299,13 @@ function () { throw new TransformationFailedException(); } $this->validator->validate($form, new Form()); + $is2Dot4Api = Validation::API_VERSION_2_4 === $this->getApiVersion(); + $this->buildViolation('invalid_message_key') ->setParameter('{{ value }}', 'foo') ->setInvalidValue('foo') ->setCode(Form::ERR_INVALID) + ->setCause($is2Dot4Api ? null : $form->getTransformationFailure()) ->assertRaised(); }