8000 Merge branch '2.8' into 3.4 · symfony/symfony@b6f9f8d · GitHub
[go: up one dir, main page]

Skip to content

Commit b6f9f8d

Browse files
Merge branch '2.8' into 3.4
* 2.8: [Form] Fixed keeping hash of equal \DateTimeInterface on submit [PhpUnitBridge] Fix typo [Config] Unset key during normalization invalidate forms on transformation failures
2 parents 8dcefc9 + 32c0172 commit b6f9f8d

File tree

11 files changed

+222
-9
lines changed

11 files changed

+222
-9
lines changed

src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class DeprecationErrorHandler
3333
* - use "/some-regexp/" to stop the test suite whenever a deprecation
3434
* message matches the given regular expression;
3535
* - use a number to define the upper bound of allowed deprecations,
36-
* making the test suite fail whenever more notices are trigerred.
36+
* making the test suite fail whenever more notices are triggered.
3737
*
3838
* @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations
3939
*/

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@
159159
<deprecated>The "%service_id%" service is deprecated since Symfony 3.1 and will be removed in 4.0.</deprecated>
160160
</service>
161161

162+
<service id="form.type_extension.form.transformation_failure_handling" class="Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension">
163+
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
164+
<argument type="service" id="translator" on-invalid="ignore" />
165+
</service>
166+
162167
<!-- FormTypeHttpFoundationExtension -->
163168
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension">
164169
<argument type="service" id="form.type_extension.form.request_handler" />

src/Symfony/Component/Config/Definition/ArrayNode.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,10 @@ protected function normalizeValue($value)
292292
$normalized = array();
293293
foreach ($value as $name => $val) {
294294
if (isset($this->children[$name])) {
295-
$normalized[$name] = $this->children[$name]->normalize($val);
295+
try {
296+
$normalized[$name] = $this->children[$name]->normalize($val);
297+
} catch (UnsetKeyException $e) {
298+
}
296299
unset($value[$name]);
297300
} elseif (!$this->removeExtraKeys) {
298301
$normalized[$name] = $val;

src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ public function thenEmptyArray()
174174
}
175175

176176
/**
177-
* Sets a closure marking the value as invalid at validation time.
177+
* Sets a closure marking the value as invalid at processing time.
178178
*
179179
* if you want to add the value of the node in your message just use a %s placeholder.
180180
*
@@ -192,7 +192,7 @@ public function thenInvalid($message)
192192
}
193193

194194
/**
195-
* Sets a closure unsetting this key of the array at validation time.
195+
* Sets a closure unsetting this key of the array at processing time.
196196
*
197197
* @return $this
198198
*

src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,25 @@ public function testNormalizeKeys()
231231
$this->assertFalse($this->getField($node, 'normalizeKeys'));
232232
}
233233

234+
public function testUnsetChild()
235+
{
236+
$node = new ArrayNodeDefinition('root');
237+
$node
238+
->children()
239+
->scalarNode('value')
240+
->beforeNormalization()
241+
->ifTrue(function ($value) {
242+
return empty($value);
243+
})
244+
->thenUnset()
245+
->end()
246+
->end()
247+
->end()
248+
;
249+
250+
$this->assertSame(array(), $node->getNode()->normalize(array('value' => null)));
251+
}
252+
234253
public function testPrototypeVariable()
235254
{
236255
$node = new ArrayNodeDefinition('root');

src/Symfony/Component/Form/Extension/Core/CoreExtension.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
1717
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
1818
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
19+
use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
1920
use Symfony\Component\PropertyAccess\PropertyAccess;
2021
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
22+
use Symfony\Component\Translation\TranslatorInterface;
2123

2224
/**
2325
* Represents the main form extension, which loads the core functionality.
@@ -28,11 +30,13 @@ class CoreExtension extends AbstractExtension
2830
{
2931
private $propertyAccessor;
3032
private $choiceListFactory;
33+
private $translator;
3134

32-
public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
35+
public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null, TranslatorInterface $translator = null)
3336
{
3437
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
3538
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
39+
$this->translator = $translator;
3640
}
3741

3842
protected function loadTypes()
@@ -74,4 +78,11 @@ protected function loadTypes()
7478
new Type\ColorType(),
7579
);
7680
}
81+
82+
protected function loadTypeExtensions()
83+
{
84+
return array(
85+
new TransformationFailureExtension($this->translator),
86+
);
87+
}
7788
}

src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,17 @@ public function mapFormsToData($forms, &$data)
7373
// Write-back is disabled if the form is not synchronized (transformation failed),
7474
// if the form was not submitted and if the form is disabled (modification not allowed)
7575
if (null !== $propertyPath && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) {
76-
// If the field is of type DateTime and the data is the same skip the update to
76+
$propertyValue = $form->getData();
77+
// If the field is of type DateTimeInterface and the data is the same skip the update to
7778
// keep the original object hash
78-
if ($form->getData() instanceof \DateTime && $form->getData() == $this->propertyAccessor->getValue($data, $propertyPath)) {
79+
if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->propertyAccessor->getValue($data, $propertyPath)) {
7980
continue;
8081
}
8182

8283
// If the data is identical to the value in $data, we are
8384
// dealing with a reference
84-
if (!\is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
85-
$this->propertyAccessor->setValue($data, $propertyPath, $form->getData());
85+
if (!\is_object($data) || !$config->getByReference() || $propertyValue !== $this->propertyAccessor->getValue($data, $propertyPath)) {
86+
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
8687
}
8788
}
8889
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Extension\Core\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\Form\FormError;
16+
use Symfony\Component\Form\FormEvent;
17+
use Symfony\Component\Form\FormEvents;
18+
use Symfony\Component\Translation\TranslatorInterface;
19+
20+
/**
21+
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
22+
*/
23+
class TransformationFailureListener implements EventSubscriberInterface
24+
{
25+
private $translator;
26+
27+
public function __construct(TranslatorInterface $translator = null)
28+
{
29+
$this->translator = $translator;
30+
}
31+
32+
public static function getSubscribedEvents()
33+
{
34+
return array(
35+
FormEvents::POST_SUBMIT => array('convertTransformationFailureToFormError', -1024),
36+
);
37+
}
38+
39+
public function convertTransformationFailureToFormError(FormEvent $event)
40+
{
41+
$form = $event->getForm();
42+
43+
if (null === $form->getTransformationFailure() || !$form->isValid()) {
44+
return;
45+
}
46+
47+
foreach ($form as $child) {
48+
if (!$child->isSynchronized()) {
49+
return;
50+
}
51+
}
52+
53+
$clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : \gettype($form->getViewData());
54+
$messageTemplate = 'The value {{ value }} is not valid.';
55+
56+
if (null !== $this->translator) {
57+
$message = $this->translator->trans($messageTemplate, array('{{ value }}' => $clientDataAsString));
58+
} else {
59+
$message = strtr($messageTemplate, array('{{ value }}' => $clientDataAsString));
60+
}
61+
62+
$form->addError(new FormError($message, $messageTemplate, array('{{ value }}' => $clientDataAsString), null, $form->getTransformationFailure()));
63+
}
64+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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\Extension\Core\Type;
13+
14+
use Symfony\Component\Form\AbstractTypeExtension;
15+
use Symfony\Component\Form\Extension\Core\EventListener\TransformationFailureListener;
16+
use Symfony\Component\Form\FormBuilderInterface;
17+
use Symfony\Component\Translation\TranslatorInterface;
18+
19+
/**
20+
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
21+
*/
22+
class TransformationFailureExtension extends AbstractTypeExtension
23+
{
24+
private $translator;
25+
26+
public function __construct(TranslatorInterface $translator = null)
27+
{
28+
$this->translator = $translator;
29+
}
30+
31+
public function buildForm(FormBuilderInterface $builder, array $options)
32+
{
33+
if (!isset($options['invalid_message']) && !isset($options['invalid_message_parameters'])) {
34+
$builder->addEventSubscriber(new TransformationFailureListener($this->translator));
35+
}
36+
}
37+
38+
public function getExtendedType()
39+
{
40+
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
41+
}
42+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\Extension\Core;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Form\Extension\Core\CoreExtension;
16+
use Symfony\Component\Form\FormFactoryBuilder;
17+
18+
class CoreExtensionTest extends TestCase
19+
{
20+
public function testTransformationFailuresAreConvertedIntoFormErrors()
21+
{
22+
$formFactoryBuilder = new FormFactoryBuilder();
23+
$formFactory = $formFactoryBuilder->addExtension(new CoreExtension())
24+
->getFormFactory();
25+
26+
$form = $formFactory->createBuilder()
27+
->add('foo', 'Symfony\Component\Form\Extension\Core\Type\DateType')
28+
->getForm();
29+
$form->submit('foo');
30+
31+
$this->assertFalse($form->isValid());
32+
}
33+
}

src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,4 +357,39 @@ public function testMapFormsToDataIgnoresDisabled()
357357

358358
$this->mapper->mapFormsToData(array($form), $car);
359359
}
360+
361+
/**
362+
* @dataProvider provideDate
363+
*/
364+
public function testMapFormsToDataDoesNotChangeEqualDateTimeInstance($date)
365+
{
366+
$article = array();
367+
$publishedAt = $date;
368+
$article['publishedAt'] = clone $publishedAt;
369+
$propertyPath = $this->getPropertyPath('[publishedAt]');
370+
371+
$this->propertyAccessor->expects($this->once())
372+
->method('getValue')
373+
->willReturn($article['publishedAt'])
374+
;
375+
$this->propertyAccessor->expects($this->never())
376+
->method('setValue')
377+
;
378+
379+
$config = new FormConfigBuilder('publishedAt', \get_class($publishedAt), $this->dispatcher);
380+
$config->setByReference(false);
381+
$config->setPropertyPath($propertyPath);
382+
$config->setData($publishedAt);
383+
$form = $this->getForm($config);
384+
385+
$this->mapper->mapFormsToData(array($form), $article);
386+
}
387+
388+
public function provideDate()
389+
{
390+
return array(
391+
array(new \DateTime()),
392+
array(new \DateTimeImmutable()),
393+
);
394+
}
360395
}

0 commit comments

Comments
 (0)
0