Skip to content

Commit 2f3bb66

Browse files
committed
feature #12054 [Form] The trace of form errors is now displayed in the profiler (webmozart)
This PR was merged into the 2.6-dev branch. Discussion ---------- [Form] The trace of form errors is now displayed in the profiler | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | yes | Deprecations? | no | Tests pass? | yes | Fixed tickets | #5607 | License | MIT | Doc PR | - This is a follow-up PR for #12052. With this change, the full trace of form errors is now displayed in the web debugger: ![error](https://cloud.githubusercontent.com/assets/176399/4419637/85facd14-456d-11e4-8c70-0e8802a586ec.png) If a violation was caused by a TransformationFailedException, the exception is now accessible through the `getCause()` method of the violation. Additionally, you can access `Form::getTransformationFailure()` to retrieve the exception. Commits ------- 8dbe258 [Form] The trace of form errors is now displayed in the profiler
2 parents 541f889 + 8dbe258 commit 2f3bb66

File tree

9 files changed

+103
-25
lines changed

9 files changed

+103
-25
lines changed

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -473,14 +473,28 @@
473473
{% endif %}
474474
</td>
475475
<td>
476-
{% if error.cause is empty %}
477-
<em>Unknown.</em>
478-
{% elseif error.cause.root is defined %}
479-
<strong>Constraint Violation</strong><br/>
480-
<pre>{{ error.cause.root }}{% if error.cause.path is not empty %}{% if error.cause.path|first != '[' %}.{% endif %}{{ error.cause.path }}{% endif %} = {{ error.cause.value }}</pre>
476+
{% for trace in error.trace %}
477+
{% if not loop.first %}
478+
<br/>Caused by:<br/><br/>
479+
{% endif %}
480+
{% if trace.root is defined %}
481+
<strong>{{ trace.class }}</strong><br/>
482+
<pre>
483+
{{- trace.root -}}
484+
{%- if trace.path is not empty -%}
485+
{%- if trace.path|first != '[' %}.{% endif -%}
486+
{{- trace.path -}}
487+
{%- endif %} = {{ trace.value -}}
488+
</pre>
489+
{% elseif trace.message is defined %}
490+
<strong>{{ trace.class }}</strong><br/>
491+
<pre>{{ trace.message }}</pre>
492+
{% else %}
493+
<pre>{{ trace }}</pre>
494+
{% endif %}
481495
{% else %}
482-
<pre>{{ error.cause }}</pre>
483-
{% endif %}
496+
<em>Unknown.</em>
497+
{% endfor %}
484498
</td>
485499
</tr>
486500
{% endfor %}

src/Symfony/Component/Form/Button.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,15 @@ public function isSynchronized()
344344
return true;
345345
}
346346

347+
/**
348+
* Unsupported method.
349+
*
350+
* @return null Always returns null
351+
*/
352+
public function getTransformationFailure()
353+
{
354+
}
355+
347356
/**
348357
* Unsupported method.
349358
*

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ CHANGELOG
2121
* ObjectChoiceList now compares choices by their value, if a value path is
2222
given
2323
* you can now pass interface names in the "data_class" option
24+
* [BC BREAK] added `FormInterface::getTransformationFailure()`
2425

2526
2.4.0
2627
-----

src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,39 @@ public function extractSubmittedData(FormInterface $form)
115115
'origin' => is_object($error->getOrigin())
116116
? spl_object_hash($error->getOrigin())
117117
: null,
118+
'trace' => array(),
118119
);
119120

120121
$cause = $error->getCause();
121122

122-
if ($cause instanceof ConstraintViolationInterface) {
123-
$errorData['cause'] = array(
124-
'root' => $this->valueExporter->exportValue($cause->getRoot()),
125-
'path' => $this->valueExporter->exportValue($cause->getPropertyPath()),
126-
'value' => $this->valueExporter->exportValue($cause->getInvalidValue()),
127-
);
128-
} else {
129-
$errorData['cause'] = null !== $cause ? $this->valueExporter->exportValue($cause) : null;
123+
while (null !== $cause) {
124+
if ($cause instanceof ConstraintViolationInterface) {
125+
$errorData['trace'][] = array(
126+
'class' => $this->valueExporter->exportValue(get_class($cause)),
127+
'root' => $this->valueExporter->exportValue($cause->getRoot()),
128+
'path' => $this->valueExporter->exportValue($cause->getPropertyPath()),
129+
'value' => $this->valueExporter->exportValue($cause->getInvalidValue()),
130+
);
131+
132+
$cause = method_exists($cause, 'getCause') ? $cause->getCause() : null;
133+
134+
continue;
135+
}
136+
137+
if ($cause instanceof \Exception) {
138+
$errorData['trace'][] = array(
139+
'class' => $this->valueExporter->exportValue(get_class($cause)),
140+
'message' => $this->valueExporter->exportValue($cause->getMessage()),
141+
);
142+
143+
$cause = $cause->getPrevious();
144+
145+
continue;
146+
}
147+
148+
$errorData['trace'][] = $cause;
149+
150+
break;
130151
}
131152

132153
$data['errors'][] = $errorData;

src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public function validate($form, Constraint $constraint)
103103
->setParameters(array_replace(array('{{ value }}' => $clientDataAsString), $config->getOption('invalid_message_parameters')))
104104
->setInvalidValue($form->getViewData())
105105
->setCode(Form::ERR_INVALID)
106+
->setCause($form->getTransformationFailure())
106107
->addViolation();
107108
}
108109
}

src/Symfony/Component/Form/Form.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,10 @@ class Form implements \IteratorAggregate, FormInterface
121121
private $extraData = array();
122122

123123
/**
124-
* Whether the data in model, normalized and view format is
125-
* synchronized. Data may not be synchronized if transformation errors
126-
* occur.
127-
* @var bool
124+
* Returns the transformation failure generated during submission, if any
125+
* @var TransformationFailedException|null
128126
*/
129-
private $synchronized = true;
127+
private $transformationFailure;
130128

131129
/**
132130
* Whether the form's data has been initialized.
@@ -634,7 +632,7 @@ public function submit($submittedData, $clearMissing = true)
634632
$viewData = $this->normToView($normData);
635633
}
636634
} catch (TransformationFailedException $e) {
637-
$this->synchronized = false;
635+
$this->transformationFailure = $e;
638636

639637
// If $viewData was not yet set, set it to $submittedData so that
640638
// the erroneous data is accessible on the form.
@@ -711,7 +709,15 @@ public function isBound()
711709
*/
712710
public function isSynchronized()
713711
{
714-
return $this->synchronized;
712+
return null === $this->transformationFailure;
713+
}
714+
715+
/**
716+
* {@inheritdoc}
717+
*/
718+
public function getTransformationFailure()
719+
{
720+
return $this->transformationFailure;
715721
}
716722

717723
/**

src/Symfony/Component/Form/FormInterface.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Form;
1313

14+
use Symfony\Component\Form\Exception\TransformationFailedException;
15+
1416
/**
1517
* A form group bundling multiple forms in a hierarchical structure.
1618
*
@@ -230,10 +232,20 @@ public function isEmpty();
230232
/**
231233
* Returns whether the data in the different formats is synchronized.
232234
*
235+
* If the data is not synchronized, you can get the transformation failure
236+
* by calling {@link getTransformationFailure()}.
237+
*
233238
* @return bool
234239
*/
235240
public function isSynchronized();
236241

242+
/**
243+
* Returns the data transformation failure, if any.
244+
*
245+
* @return TransformationFailedException|null The transformation failure
246+
*/
247+
public function getTransformationFailure();
248+
237249
/**
238250
* Initializes the form tree.
239251
*

src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ public function testExtractSubmittedDataStoresErrors()
319319
'norm' => "'Foobar'",
320320
),
321321
'errors' => array(
322-
array('message' => 'Invalid!', 'origin' => null, 'cause' => null),
322+
array('message' => 'Invalid!', 'origin' => null, 'trace' => array()),
323323
),
324324
'synchronized' => 'true',
325325
), $this->dataExtractor->extractSubmittedData($form));
@@ -340,7 +340,7 @@ public function testExtractSubmittedDataStoresErrorOrigin()
340340
'norm' => "'Foobar'",
341341
),
342342
'errors' => array(
343-
array('message' => 'Invalid!', 'origin' => spl_object_hash($form), 'cause' => null),
343+
array('message' => 'Invalid!', 'origin' => spl_object_hash($form), 'trace' => array()),
344344
),
345345
'synchronized' => 'true',
346346
), $this->dataExtractor->extractSubmittedData($form));
@@ -360,7 +360,12 @@ public function testExtractSubmittedDataStoresErrorCause()
360360
'norm' => "'Foobar'",
361361
),
362362
'errors' => array(
363-
array('message' => 'Invalid!', 'origin' => null, 'cause' => 'object(Exception)'),
363+
array('message' => 'Invalid!', 'origin' => null, 'trace' => array(
364+
array(
365+
'class' => "'Exception'",
366+
'message' => "''",
367+
),
368+
)),
364369
),
365370
'synchronized' => 'true',
366371
), $this->dataExtractor->extractSubmittedData($form));

src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,14 @@ function () { throw new TransformationFailedException(); }
225225

226226
$this->validator->validate($form, new Form());
227227

228+
$is2Dot4Api = Validation::API_VERSION_2_4 === $this->getApiVersion();
229+
228230
$this->buildViolation('invalid_message_key')
229231
->setParameter('{{ value }}', 'foo')
230232
->setParameter('{{ foo }}', 'bar')
231233
->setInvalidValue('foo')
232234
->setCode(Form::ERR_INVALID)
235+
->setCause($is2Dot4Api ? null : $form->getTransformationFailure())
233236
->assertRaised();
234237
}
235238

@@ -259,11 +262,14 @@ function () { throw new TransformationFailedException(); }
259262

260263
$this->validator->validate($form, new Form());
261264

265+
$is2Dot4Api = Validation::API_VERSION_2_4 === $this->getApiVersion();
266+
262267
$this->buildViolation('invalid_message_key')
263268
->setParameter('{{ value }}', 'foo')
264269
->setParameter('{{ foo }}', 'bar')
265270
->setInvalidValue('foo')
266271
->setCode(Form::ERR_INVALID)
272+
->setCause($is2Dot4Api ? null : $form->getTransformationFailure())
267273
->assertRaised();
268274
}
269275

@@ -293,10 +299,13 @@ function () { throw new TransformationFailedException(); }
293299

294300
$this->validator->validate($form, new Form());
295301

302+
$is2Dot4Api = Validation::API_VERSION_2_4 === $this->getApiVersion();
303+
296304
$this->buildViolation('invalid_message_key')
297305
->setParameter('{{ value }}', 'foo')
298306
->setInvalidValue('foo')
299307
->setCode(Form::ERR_INVALID)
308+
->setCause($is2Dot4Api ? null : $form->getTransformationFailure())
300309
->assertRaised();
301310
}
302311

0 commit comments

Comments
 (0)