Skip to content

Commit dcb8d8b

Browse files
committed
[Serializer] Adds FormErrorNormalizer
1 parent 9f0eee1 commit dcb8d8b

File tree

6 files changed

+279
-0
lines changed

6 files changed

+279
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Symfony\Component\Form\FormRegistryInterface;
3535
use Symfony\Component\Form\ResolvedFormTypeFactory;
3636
use Symfony\Component\Form\ResolvedFormTypeFactoryInterface;
37+
use Symfony\Component\Form\Serializer\FormErrorNormalizer;
3738
use Symfony\Component\Form\Util\ServerParams;
3839

3940
return static function (ContainerConfigurator $container) {
@@ -140,5 +141,8 @@
140141
param('validator.translation_domain'),
141142
])
142143
->tag('form.type_extension')
144+
145+
->set('form.serializer.normalizer.form_error', FormErrorNormalizer::class)
146+
->tag('serializer.normalizer', ['priority' => -915])
143147
;
144148
};

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
4141
use Symfony\Component\DependencyInjection\Reference;
4242
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
43+
use Symfony\Component\Form\Serializer\FormErrorNormalizer;
4344
use Symfony\Component\HttpClient\ScopingHttpClient;
4445
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
4546
use Symfony\Component\Messenger\Transport\TransportFactory;
@@ -1150,6 +1151,17 @@ public function testDateTimeNormalizerRegistered()
11501151
$this->assertEquals(-910, $tag[0]['priority']);
11511152
}
11521153

1154+
public function testFormErrorNormalizerRegistred()
1155+
{
1156+
$container = $this->createContainerFromFile('full');
1157+
1158+
$definition = $container->getDefinition('form.serializer.normalizer.form_error');
1159+
$tag = $definition->getTag('serializer.normalizer');
1160+
1161+
$this->assertEquals(FormErrorNormalizer::class, $definition->getClass());
1162+
$this->assertEquals(-915, $tag[0]['priority']);
1163+
}
1164+
11531165
public function testJsonSerializableNormalizerRegistered()
11541166
{
11551167
$container = $this->createContainerFromFile('full');

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.2.0
5+
-----
6+
7+
* added `FormErrorNormalizer`
8+
49
5.1.0
510
-----
611

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Serializer;
13+
14+
use Symfony\Component\Form\FormInterface;
15+
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
16+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
17+
18+
/**
19+
* Normalizes invalid Form instances.
20+
*/
21+
final class FormErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
22+
{
23+
const TITLE = 'title';
24+
const TYPE = 'type';
25+
const CODE = 'status_code';
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public function normalize($object, $format = null, array $context = []): array
31+
{
32+
$data = [
33+
'title' => $context[self::TITLE] ?? 'Validation Failed',
34+
'type' => $context[self::TYPE] ?? 'https://symfony.com/errors/form',
35+
'code' => $context[self::CODE] ?? null,
36+
'errors' => $this->convertFormErrorsToArray($object),
37+
];
38+
39+
if (0 !== \count($object->all())) {
40+
$data['children'] = $this->convertFormChildrenToArray($object);
41+
}
42+
43+
return $data;
44+
}
45+
46+
/**
47+
* {@inheritdoc}
48+
*/
49+
public function supportsNormalization($data, $format = null): bool
50+
{
51+
return $data instanceof FormInterface && $data->isSubmitted() && !$data->isValid();
52+
}
53+
54+
private function convertFormErrorsToArray(FormInterface $data): array
55+
{
56+
$errors = [];
57+
58+
foreach ($data->getErrors() as $error) {
59+
$errors[] = [
60+
'message' => $error->getMessage(),
61+
'cause' => $error->getCause(),
62+
];
63+
}
64+
65+
return $errors;
66+
}
67+
68+
private function convertFormChildrenToArray(FormInterface $data): array
69+
{
70+
$children = [];
71+
72+
foreach ($data->all() as $child) {
73+
$childData = [
74+
'errors' => $this->convertFormErrorsToArray($child),
75+
];
76+
77+
if (!empty($child->all())) {
78+
$childData['children'] = $this->convertFormChildrenToArray($child);
79+
}
80+
81+
$children[$child->getName()] = $childData;
82+
}
83+
84+
return $children;
85+
}
86+
87+
/**
88+
* {@inheritdoc}
89+
*/
90+
public function hasCacheableSupportsMethod(): bool
91+
{
92+
return __CLASS__ === static::class;
93+
}
94+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Tests\Serializer;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Form\FormError;
16+
use Symfony\Component\Form\FormErrorIterator;
17+
use Symfony\Component\Form\FormInterface;
18+
use Symfony\Component\Form\Serializer\FormErrorNormalizer;
19+
20+
class FormErrorNormalizerTest extends TestCase
21+
{
22+
/**
23+
* @var FormErrorNormalizer
24+
*/
25+
private $normalizer;
26+
27+
/**
28+
* @var FormInterface
29+
*/
30+
private $form;
31+
32+
protected function setUp(): void
33+
{
34+
$this->normalizer = new FormErrorNormalizer();
35+
36+
$this->form = $this->createMock(FormInterface::class);
37+
$this->form->method('isSubmitted')->willReturn(true);
38+
$this->form->method('all')->willReturn([]);
39+
40+
$this->form->method('getErrors')
41+
->willReturn(new FormErrorIterator($this->form, [
42+
new FormError('a', 'b', ['c', 'd'], 5, 'f'),
43+
new FormError(1, 2, [3, 4], 5, 6),
44+
])
45+
);
46+
}
47+
48+
public function testSupportsNormalizationWithWrongClass()
49+
{
50+
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
51+
}
52+
53+
public function testSupportsNormalizationWithNotSubmittedForm()
54+
{
55+
$form = $this->createMock(FormInterface::class);
56+
$this->assertFalse($this->normalizer->supportsNormalization($form));
57+
}
58+
59+
public function testSupportsNormalizationWithValidForm()
60+
{
61+
$this->assertTrue($this->normalizer->supportsNormalization($this->form));
62+
}
63+
64+
public function testNormalize()
65+
{
66+
$expected = [
67+
'code' => null,
68+
'title' => 'Validation Failed',
69+
'type' => 'https://symfony.com/errors/form',
70+
'errors' => [
71+
[
72+
'message' => 'a',
73+
'cause' => 'f',
74+
],
75+
[
76+
'message' => '1',
77+
'cause' => 6,
78+
],
79+
],
80+
];
81+
82+
$this->assertEquals($expected, $this->normalizer->normalize($this->form));
83+
}
84+
85+
public function testNormalizeWithChildren()
86+
{
87+
$exptected = [
88+
'code' => null,
89+
'title' => 'Validation Failed',
90+
'type' => 'https://symfony.com/errors/form',
91+
'errors' => [
92+
[
93+
'message' => 'a',
94+
'cause' => null,
95+
],
96+
],
97+
'children' => [
98+
'form1' => [
99+
'errors' => [
100+
[
101+
'message' => 'b',
102+
'cause' => null,
103+
],
104+
],
105+
],
106+
'form2' => [
107+
'errors' => [
108+
[
109+
'message' => 'c',
110+
'cause' => null,
111+
],
112+
],
113+
'children' => [
114+
'form3' => [
115+
'errors' => [
116+
[
117+
'message' => 'd',
118+
'cause' => null,
119+
],
120+
],
121+
],
122+
],
123+
],
124+
],
125+
];
126+
127+
$form = clone $form1 = clone $form2 = clone $form3 = $this->createMock(FormInterface::class);
128+
129+
$form1->method('getErrors')
130+
->willReturn(new FormErrorIterator($form1, [
131+
new FormError('b'),
132+
])
133+
);
134+
$form1->method('getName')->willReturn('form1');
135+
136+
$form2->method('getErrors')
137+
->willReturn(new FormErrorIterator($form1, [
138+
new FormError('c'),
139+
])
140+
);
141+
$form2->method('getName')->willReturn('form2');
142+
143+
$form3->method('getErrors')
144+
->willReturn(new FormErrorIterator($form1, [
145+
new FormError('d'),
146+
])
147+
);
148+
$form3->method('getName')->willReturn('form3');
149+
150+
$form2->method('all')->willReturn([$form3]);
151+
152+
$form = $this->createMock(FormInterface::class);
153+
$form->method('isSubmitted')->willReturn(true);
154+
$form->method('all')->willReturn([$form1, $form2]);
155+
$form->method('getErrors')
156+
->willReturn(new FormErrorIterator($form, [
157+
new FormError('a'),
158+
])
159+
);
160+
161+
$this->assertEquals($exptected, $this->normalizer->normalize($form));
162+
}
163+
}

src/Symfony/Component/Form/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"symfony/http-foundation": "^4.4|^5.0",
3838
"symfony/http-kernel": "^4.4|^5.0",
3939
"symfony/security-csrf": "^4.4|^5.0",
40+
"symfony/serializer": "^4.4|^5.0",
4041
"symfony/translation": "^4.4|^5.0",
4142
"symfony/var-dumper": "^4.4|^5.0"
4243
},

0 commit comments

Comments
 (0)