From 385d9df29cdf8a34df4d8f95258d97e85ca3dbae Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 4 Oct 2018 21:52:40 +0200 Subject: [PATCH 1/4] invalidate forms on transformation failures --- .../FrameworkBundle/Resources/config/form.xml | 5 ++ .../Form/Extension/Core/CoreExtension.php | 13 +++- .../TransformationFailureListener.php | 64 +++++++++++++++++++ .../Type/TransformationFailureExtension.php | 42 ++++++++++++ .../Extension/Core/CoreExtensionTest.php | 33 ++++++++++ 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Form/Extension/Core/EventListener/TransformationFailureListener.php create mode 100644 src/Symfony/Component/Form/Extension/Core/Type/TransformationFailureExtension.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Core/CoreExtensionTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index 7e86fb0728..b0bcf00443 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -166,6 +166,11 @@ + + + + + diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php index fc9af48891..e2e40e4634 100644 --- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php +++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php @@ -16,8 +16,10 @@ use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Translation\TranslatorInterface; /** * Represents the main form extension, which loads the core functionality. @@ -28,11 +30,13 @@ class CoreExtension extends AbstractExtension { private $propertyAccessor; private $choiceListFactory; + private $translator; - public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) + public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null, TranslatorInterface $translator = null) { $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); + $this->translator = $translator; } protected function loadTypes() @@ -71,4 +75,11 @@ class CoreExtension extends AbstractExtension new Type\CurrencyType(), ); } + + protected function loadTypeExtensions() + { + return array( + new TransformationFailureExtension($this->translator), + ); + } } diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/TransformationFailureListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/TransformationFailureListener.php new file mode 100644 index 0000000000..f46eb499e0 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/TransformationFailureListener.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * @author Christian Flothmann + */ +class TransformationFailureListener implements EventSubscriberInterface +{ + private $translator; + + public function __construct(TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + + public static function getSubscribedEvents() + { + return array( + FormEvents::POST_SUBMIT => array('convertTransformationFailureToFormError', -1024), + ); + } + + public function convertTransformationFailureToFormError(FormEvent $event) + { + $form = $event->getForm(); + + if (null === $form->getTransformationFailure() || !$form->isValid()) { + return; + } + + foreach ($form as $child) { + if (!$child->isSynchronized()) { + return; + } + } + + $clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : \gettype($form->getViewData()); + $messageTemplate = 'The value {{ value }} is not valid.'; + + if (null !== $this->translator) { + $message = $this->translator->trans($messageTemplate, array('{{ value }}' => $clientDataAsString)); + } else { + $message = strtr($messageTemplate, array('{{ value }}' => $clientDataAsString)); + } + + $form->addError(new FormError($message, $messageTemplate, array('{{ value }}' => $clientDataAsString), null, $form->getTransformationFailure())); + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TransformationFailureExtension.php b/src/Symfony/Component/Form/Extension/Core/Type/TransformationFailureExtension.php new file mode 100644 index 0000000000..98875594d6 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/Type/TransformationFailureExtension.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\EventListener\TransformationFailureListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * @author Christian Flothmann + */ +class TransformationFailureExtension extends AbstractTypeExtension +{ + private $translator; + + public function __construct(TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!isset($options['invalid_message']) && !isset($options['invalid_message_parameters'])) { + $builder->addEventSubscriber(new TransformationFailureListener($this->translator)); + } + } + + public function getExtendedType() + { + return 'Symfony\Component\Form\Extension\Core\Type\FormType'; + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/CoreExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/CoreExtensionTest.php new file mode 100644 index 0000000000..ff85149e21 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/CoreExtensionTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Core; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\Extension\Core\CoreExtension; +use Symfony\Component\Form\FormFactoryBuilder; + +class CoreExtensionTest extends TestCase +{ + public function testTransformationFailuresAreConvertedIntoFormErrors() + { + $formFactoryBuilder = new FormFactoryBuilder(); + $formFactory = $formFactoryBuilder->addExtension(new CoreExtension()) + ->getFormFactory(); + + $form = $formFactory->createBuilder() + ->add('foo', 'Symfony\Component\Form\Extension\Core\Type\DateType') + ->getForm(); + $form->submit('foo'); + + $this->assertFalse($form->isValid()); + } +} From e1402d495e70c0e03f28bce186fe946a31d81e31 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Fri, 9 Nov 2018 11:22:20 +0100 Subject: [PATCH 2/4] [Config] Unset key during normalization --- .../Component/Config/Definition/ArrayNode.php | 5 ++++- .../Config/Definition/Builder/ExprBuilder.php | 4 ++-- .../Builder/ArrayNodeDefinitionTest.php | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Config/Definition/ArrayNode.php b/src/Symfony/Component/Config/Definition/ArrayNode.php index c8cb82c41f..13fe1c94bc 100644 --- a/src/Symfony/Component/Config/Definition/ArrayNode.php +++ b/src/Symfony/Component/Config/Definition/ArrayNode.php @@ -288,7 +288,10 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface $normalized = array(); foreach ($value as $name => $val) { if (isset($this->children[$name])) { - $normalized[$name] = $this->children[$name]->normalize($val); + try { + $normalized[$name] = $this->children[$name]->normalize($val); + } catch (UnsetKeyException $e) { + } unset($value[$name]); } elseif (!$this->removeExtraKeys) { $normalized[$name] = $val; diff --git a/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php b/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php index fedbe0cc1b..ddbe5b0401 100644 --- a/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php +++ b/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php @@ -149,7 +149,7 @@ class ExprBuilder } /** - * Sets a closure marking the value as invalid at validation time. + * Sets a closure marking the value as invalid at processing time. * * if you want to add the value of the node in your message just use a %s placeholder. * @@ -167,7 +167,7 @@ class ExprBuilder } /** - * Sets a closure unsetting this key of the array at validation time. + * Sets a closure unsetting this key of the array at processing time. * * @return $this * diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php index 482ead310b..05fcd94e90 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php @@ -231,6 +231,25 @@ class ArrayNodeDefinitionTest extends TestCase $this->assertFalse($this->getField($node, 'normalizeKeys')); } + public function testUnsetChild() + { + $node = new ArrayNodeDefinition('root'); + $node + ->children() + ->scalarNode('value') + ->beforeNormalization() + ->ifTrue(function ($value) { + return empty($value); + }) + ->thenUnset() + ->end() + ->end() + ->end() + ; + + $this->assertSame(array(), $node->getNode()->normalize(array('value' => null))); + } + public function getEnableableNodeFixtures() { return array( From 78e386e87f369734c96a0873b05d9e370e350dd9 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Mon, 12 Nov 2018 19:05:42 +0100 Subject: [PATCH 3/4] [PhpUnitBridge] Fix typo --- src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index f95d1389ff..7a48f1a175 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -30,7 +30,7 @@ class DeprecationErrorHandler * - use "/some-regexp/" to stop the test suite whenever a deprecation * message matches the given regular expression; * - use a number to define the upper bound of allowed deprecations, - * making the test suite fail whenever more notices are trigerred. + * making the test suite fail whenever more notices are triggered. * * @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations */ From bc2e2cb5ad8a347cbab6162ce9157b0af02ec426 Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Mon, 12 Nov 2018 11:48:30 +0100 Subject: [PATCH 4/4] [Form] Fixed keeping hash of equal \DateTimeInterface on submit --- .../Core/DataMapper/PropertyPathMapper.php | 9 +++-- .../DataMapper/PropertyPathMapperTest.php | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index f9721e52b1..e8dbabdb9f 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -73,16 +73,17 @@ class PropertyPathMapper implements DataMapperInterface // Write-back is disabled if the form is not synchronized (transformation failed), // if the form was not submitted and if the form is disabled (modification not allowed) if (null !== $propertyPath && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) { - // If the field is of type DateTime and the data is the same skip the update to + $propertyValue = $form->getData(); + // If the field is of type DateTime or DateTimeInterface and the data is the same skip the update to // keep the original object hash - if ($form->getData() instanceof \DateTime && $form->getData() == $this->propertyAccessor->getValue($data, $propertyPath)) { + if (($propertyValue instanceof \DateTime || $propertyValue instanceof \DateTimeInterface) && $propertyValue == $this->propertyAccessor->getValue($data, $propertyPath)) { continue; } // If the data is identical to the value in $data, we are // dealing with a reference - if (!\is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) { - $this->propertyAccessor->setValue($data, $propertyPath, $form->getData()); + if (!\is_object($data) || !$config->getByReference() || $propertyValue !== $this->propertyAccessor->getValue($data, $propertyPath)) { + $this->propertyAccessor->setValue($data, $propertyPath, $propertyValue); } } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php index 979adcdcee..1dcff2f402 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php @@ -357,4 +357,44 @@ class PropertyPathMapperTest extends TestCase $this->mapper->mapFormsToData(array($form), $car); } + + /** + * @dataProvider provideDate + */ + public function testMapFormsToDataDoesNotChangeEqualDateTimeInstance($date) + { + $article = array(); + $publishedAt = $date; + $article['publishedAt'] = clone $publishedAt; + $propertyPath = $this->getPropertyPath('[publishedAt]'); + + $this->propertyAccessor->expects($this->once()) + ->method('getValue') + ->willReturn($article['publishedAt']) + ; + $this->propertyAccessor->expects($this->never()) + ->method('setValue') + ; + + $config = new FormConfigBuilder('publishedAt', \get_class($publishedAt), $this->dispatcher); + $config->setByReference(false); + $config->setPropertyPath($propertyPath); + $config->setData($publishedAt); + $form = $this->getForm($config); + + $this->mapper->mapFormsToData(array($form), $article); + } + + public function provideDate() + { + $data = array( + '\DateTime' => array(new \DateTime()), + ); + + if (class_exists('DateTimeImmutable')) { + $data['\DateTimeImmutable'] = array(new \DateTimeImmutable()); + } + + return $data; + } }