Skip to content

Commit afeb7b8

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 e0dbf87 + 8474340 commit afeb7b8

File tree

6 files changed

+191
-5
lines changed

6 files changed

+191
-5
lines changed

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
}

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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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+
}

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)