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
This commit is contained in:
Nicolas Grekas 2018-11-13 17:52:15 +01:00
commit b6f9f8d769
11 changed files with 222 additions and 9 deletions

View File

@ -33,7 +33,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
*/

View File

@ -159,6 +159,11 @@
<deprecated>The "%service_id%" service is deprecated since Symfony 3.1 and will be removed in 4.0.</deprecated>
</service>
<service id="form.type_extension.form.transformation_failure_handling" class="Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
<argument type="service" id="translator" on-invalid="ignore" />
</service>
<!-- FormTypeHttpFoundationExtension -->
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension">
<argument type="service" id="form.type_extension.form.request_handler" />

View File

@ -292,7 +292,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;

View File

@ -174,7 +174,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.
*
@ -192,7 +192,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
*

View File

@ -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 testPrototypeVariable()
{
$node = new ArrayNodeDefinition('root');

View File

@ -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()
@ -74,4 +78,11 @@ class CoreExtension extends AbstractExtension
new Type\ColorType(),
);
}
protected function loadTypeExtensions()
{
return array(
new TransformationFailureExtension($this->translator),
);
}
}

View File

@ -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 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 \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);
}
}
}

View File

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <christian.flothmann@sensiolabs.de>
*/
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()));
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <christian.flothmann@sensiolabs.de>
*/
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';
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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());
}
}

View File

@ -357,4 +357,39 @@ 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()
{
return array(
array(new \DateTime()),
array(new \DateTimeImmutable()),
);
}
}