Skip to content

Commit 8ea3a43

Browse files
committed
feature #9993 [Form] Errors now reference the field they were added to and the violation/exception that caused them (bschussek)
This PR was merged into the 2.5-dev branch. Discussion ---------- [Form] Errors now reference the field they were added to and the violation/exception that caused them | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #9472, #9582 | License | MIT | Doc PR | - Commits ------- c8a0ee6 [Form] Errors now reference the field they were added to and the violation/exception that caused them
2 parents 147c82b + c8a0ee6 commit 8ea3a43

File tree

10 files changed

+384
-130
lines changed

10 files changed

+384
-130
lines changed

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

+24-5
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@
191191
</div>
192192

193193
{% for formName, formData in collector.data.forms %}
194-
{{ form_tree_details(formName, formData) }}
194+
{{ form_tree_details(formName, formData, collector.data.forms_by_hash) }}
195195
{% endfor %}
196196
</div>
197197
{% else %}
@@ -366,7 +366,7 @@
366366
</li>
367367
{% endmacro %}
368368

369-
{% macro form_tree_details(name, data) %}
369+
{% macro form_tree_details(name, data, forms_by_hash) %}
370370
<div class="tree-details" id="{{ data.id }}-details">
371371
<h2>
372372
{{ name }}
@@ -386,13 +386,32 @@
386386

387387
<table id="{{ data.id }}-errors">
388388
<tr>
389-
<th width="50%">Message</th>
389+
<th>Message</th>
390+
<th>Origin</th>
390391
<th>Cause</th>
391392
</tr>
392393
{% for error in data.errors %}
393394
<tr>
394395
<td>{{ error.message }}</td>
395-
<td><em>Unknown.</em></td>
396+
<td>
397+
{% if error.origin is empty %}
398+
<em>This form.</em>
399+
{% elseif forms_by_hash[error.origin] is not defined %}
400+
<em>Unknown.</em>
401+
{% else %}
402+
{{ forms_by_hash[error.origin].name }}
403+
{% endif %}
404+
</td>
405+
<td>
406+
{% if error.cause is empty %}
407+
<em>Unknown.</em>
408+
{% elseif error.cause.root is defined %}
409+
<strong>Constraint Violation</strong><br/>
410+
<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>
411+
{% else %}
412+
<pre>{{ error.cause }}</pre>
413+
{% endif %}
414+
</td>
396415
</tr>
397416
{% endfor %}
398417
</table>
@@ -565,6 +584,6 @@
565584
</div>
566585

567586
{% for childName, childData in data.children %}
568-
{{ _self.form_tree_details(childName, childData) }}
587+
{{ _self.form_tree_details(childName, childData, forms_by_hash) }}
569588
{% endfor %}
570589
{% endmacro %}

src/Symfony/Component/Form/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CHANGELOG
55
------
66

77
* added an option for multiple files upload
8+
* form errors now reference their cause (constraint violation, exception, ...)
9+
* form errors now remember which form they were originally added to
810

911
2.4.0
1012
-----

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

+11-6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public function __construct(FormDataExtractorInterface $dataExtractor)
6969
$this->dataExtractor = $dataExtractor;
7070
$this->data = array(
7171
'forms' => array(),
72+
'forms_by_hash' => array(),
7273
'nb_errors' => 0,
7374
);
7475
}
@@ -184,7 +185,7 @@ public function buildPreliminaryFormTree(FormInterface $form)
184185
{
185186
$this->data['forms'][$form->getName()] = array();
186187

187-
$this->recursiveBuildPreliminaryFormTree($form, $this->data['forms'][$form->getName()]);
188+
$this->recursiveBuildPreliminaryFormTree($form, $this->data['forms'][$form->getName()], $this->data['forms_by_hash']);
188189
}
189190

190191
/**
@@ -194,7 +195,7 @@ public function buildFinalFormTree(FormInterface $form, FormView $view)
194195
{
195196
$this->data['forms'][$form->getName()] = array();
196197

197-
$this->recursiveBuildFinalFormTree($form, $view, $this->data['forms'][$form->getName()]);
198+
$this->recursiveBuildFinalFormTree($form, $view, $this->data['forms'][$form->getName()], $this->data['forms_by_hash']);
198199
}
199200

200201
/**
@@ -213,24 +214,26 @@ public function getData()
213214
return $this->data;
214215
}
215216

216-
private function recursiveBuildPreliminaryFormTree(FormInterface $form, &$output = null)
217+
private function recursiveBuildPreliminaryFormTree(FormInterface $form, &$output = null, array &$outputByHash)
217218
{
218219
$hash = spl_object_hash($form);
219220

220221
$output = isset($this->dataByForm[$hash])
221222
? $this->dataByForm[$hash]
222223
: array();
223224

225+
$outputByHash[$hash] = &$output;
226+
224227
$output['children'] = array();
225228

226229
foreach ($form as $name => $child) {
227230
$output['children'][$name] = array();
228231

229-
$this->recursiveBuildPreliminaryFormTree($child, $output['children'][$name]);
232+
$this->recursiveBuildPreliminaryFormTree($child, $output['children'][$name], $outputByHash);
230233
}
231234
}
232235

233-
private function recursiveBuildFinalFormTree(FormInterface $form = null, FormView $view, &$output = null)
236+
private function recursiveBuildFinalFormTree(FormInterface $form = null, FormView $view, &$output = null, array &$outputByHash)
234237
{
235238
$viewHash = spl_object_hash($view);
236239
$formHash = null;
@@ -255,6 +258,8 @@ private function recursiveBuildFinalFormTree(FormInterface $form = null, FormVie
255258
? $this->dataByForm[$formHash]
256259
: array()
257260
);
261+
262+
$outputByHash[$formHash] = &$output;
258263
}
259264

260265
$output['children'] = array();
@@ -268,7 +273,7 @@ private function recursiveBuildFinalFormTree(FormInterface $form = null, FormVie
268273

269274
$output['children'][$name] = array();
270275

271-
$this->recursiveBuildFinalFormTree($childForm, $childView, $output['children'][$name]);
276+
$this->recursiveBuildFinalFormTree($childForm, $childView, $output['children'][$name], $outputByHash);
272277
}
273278
}
274279
}

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

+26-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Form\FormInterface;
1515
use Symfony\Component\Form\FormView;
1616
use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter;
17+
use Symfony\Component\Validator\ConstraintViolationInterface;
1718

1819
/**
1920
* Default implementation of {@link FormDataExtractorInterface}.
@@ -43,6 +44,7 @@ public function extractConfiguration(FormInterface $form)
4344
{
4445
$data = array(
4546
'id' => $this->buildId($form),
47+
'name' => $form->getName(),
4648
'type' => $form->getConfig()->getType()->getName(),
4749
'type_class' => get_class($form->getConfig()->getType()->getInnerType()),
4850
'synchronized' => $this->valueExporter->exportValue($form->isSynchronized()),
@@ -108,9 +110,26 @@ public function extractSubmittedData(FormInterface $form)
108110
}
109111

110112
foreach ($form->getErrors() as $error) {
111-
$data['errors'][] = array(
113+
$errorData = array(
112114
'message' => $error->getMessage(),
115+
'origin' => is_object($error->getOrigin())
116+
? spl_object_hash($error->getOrigin())
117+
: null,
113118
);
119+
120+
$cause = $error->getCause();
121+
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;
130+
}
131+
132+
$data['errors'][] = $errorData;
114133
}
115134

116135
$data['synchronized'] = $this->valueExporter->exportValue($form->isSynchronized());
@@ -127,8 +146,12 @@ public function extractViewVariables(FormView $view)
127146

128147
// Set the ID in case no FormInterface object was collected for this
129148
// view
130-
if (isset($view->vars['id'])) {
131-
$data['id'] = $view->vars['id'];
149+
if (!isset($data['id'])) {
150+
$data['id'] = isset($view->vars['id']) ? $view->vars['id'] : null;
151+
}
152+
153+
if (!isset($data['name'])) {
154+
$data['name'] = isset($view->vars['name']) ? $view->vars['name'] : null;
132155
}
133156

134157
foreach ($view->vars as $varName => $value) {

src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form
128128
$violation->getMessage(),
129129
$violation->getMessageTemplate(),
130130
$violation->getMessageParameters(),
131-
$violation->getMessagePluralization()
131+
$violation->getMessagePluralization(),
132+
$violation
132133
));
133134
}
134135
}

src/Symfony/Component/Form/Form.php

+4
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,10 @@ public function bind($submittedData)
673673
public function addError(FormError $error)
674674
{
675675
if ($this->parent && $this->config->getErrorBubbling()) {
676+
if (null === $error->getOrigin()) {
677+
$error->setOrigin($this);
678+
}
679+
676680
$this->parent->addError($error);
677681
} else {
678682
$this->errors[] = $error;

src/Symfony/Component/Form/FormError.php

+83-3
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111

1212
namespace Symfony\Component\Form;
1313

14+
use Symfony\Component\Form\Exception\BadMethodCallException;
15+
1416
/**
1517
* Wraps errors in forms
1618
*
1719
* @author Bernhard Schussek <bschussek@gmail.com>
1820
*/
19-
class FormError
21+
class FormError implements \Serializable
2022
{
2123
/**
2224
* @var string
@@ -41,6 +43,18 @@ class FormError
4143
*/
4244
protected $messagePluralization;
4345

46+
/**
47+
* The cause for this error
48+
* @var mixed
49+
*/
50+
private $cause;
51+
52+
/**
53+
* The form that spawned this error
54+
* @var FormInterface
55+
*/
56+
private $origin;
57+
4458
/**
4559
* Constructor
4660
*
@@ -50,17 +64,19 @@ class FormError
5064
* @param string $message The translated error message
5165
* @param string|null $messageTemplate The template for the error message
5266
* @param array $messageParameters The parameters that should be
53-
* substituted in the message template.
67+
* substituted in the message template
5468
* @param integer|null $messagePluralization The value for error message pluralization
69+
* @param mixed $cause The cause of the error
5570
*
5671
* @see \Symfony\Component\Translation\Translator
5772
*/
58-
public function __construct($message, $messageTemplate = null, array $messageParameters = array(), $messagePluralization = null)
73+
public function __construct($message, $messageTemplate = null, array $messageParameters = array(), $messagePluralization = null, $cause = null)
5974
{
6075
$this->message = $message;
6176
$this->messageTemplate = $messageTemplate ?: $message;
6277
$this->messageParameters = $messageParameters;
6378
$this->messagePluralization = $messagePluralization;
79+
$this->cause = $cause;
6480
}
6581

6682
/**
@@ -102,4 +118,68 @@ public function getMessagePluralization()
102118
{
103119
return $this->messagePluralization;
104120
}
121+
122+
/**
123+
* Returns the cause of this error.
124+
*
125+
* @return mixed The cause of this error
126+
*/
127+
public function getCause()
128+
{
129+
return $this->cause;
130+
}
131+
132+
/**
133+
* Sets the form that caused this error.
134+
*
135+
* This method must only be called once.
136+
*
137+
* @param FormInterface $origin The form that caused this error
138+
*
139+
* @throws BadMethodCallException If the method is called more than once
140+
*/
141+
public function setOrigin(FormInterface $origin)
142+
{
143+
if (null !== $this->origin) {
144+
throw new BadMethodCallException('setOrigin() must only be called once.');
145+
}
146+
147+
$this->origin = $origin;
148+
}
149+
150+
/**
151+
* Returns the form that caused this error.
152+
*
153+
* @return FormInterface The form that caused this error
154+
*/
155+
public function getOrigin()
156+
{
157+
return $this->origin;
158+
}
159+
160+
/**
161+
* Serializes this error.
162+
*
163+
* @return string The serialized error
164+
*/
165+
public function serialize()
166+
{
167+
return serialize(array(
168+
$this->message,
169+
$this->messageTemplate,
170+
$this->messageParameters,
171+
$this->messagePluralization,
172+
$this->cause
173+
));
174+
}
175+
176+
/**
177+
* Unserializes a serialized error.
178+
*
179+
* @param string $serialized The serialized error
180+
*/
181+
public function unserialize($serialized)
182+
{
183+
list($this->message, $this->messageTemplate, $this->messageParameters, $this->messagePluralization, $this->cause) = unserialize($serialized);
184+
}
105185
}

0 commit comments

Comments
 (0)