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:
commit
b6f9f8d769
@ -33,7 +33,7 @@ class DeprecationErrorHandler
|
|||||||
* - use "/some-regexp/" to stop the test suite whenever a deprecation
|
* - use "/some-regexp/" to stop the test suite whenever a deprecation
|
||||||
* message matches the given regular expression;
|
* message matches the given regular expression;
|
||||||
* - use a number to define the upper bound of allowed deprecations,
|
* - 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
|
* @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations
|
||||||
*/
|
*/
|
||||||
|
@ -159,6 +159,11 @@
|
|||||||
<deprecated>The "%service_id%" service is deprecated since Symfony 3.1 and will be removed in 4.0.</deprecated>
|
<deprecated>The "%service_id%" service is deprecated since Symfony 3.1 and will be removed in 4.0.</deprecated>
|
||||||
</service>
|
</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 -->
|
<!-- FormTypeHttpFoundationExtension -->
|
||||||
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\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" />
|
<argument type="service" id="form.type_extension.form.request_handler" />
|
||||||
|
@ -292,7 +292,10 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface
|
|||||||
$normalized = array();
|
$normalized = array();
|
||||||
foreach ($value as $name => $val) {
|
foreach ($value as $name => $val) {
|
||||||
if (isset($this->children[$name])) {
|
if (isset($this->children[$name])) {
|
||||||
|
try {
|
||||||
$normalized[$name] = $this->children[$name]->normalize($val);
|
$normalized[$name] = $this->children[$name]->normalize($val);
|
||||||
|
} catch (UnsetKeyException $e) {
|
||||||
|
}
|
||||||
unset($value[$name]);
|
unset($value[$name]);
|
||||||
} elseif (!$this->removeExtraKeys) {
|
} elseif (!$this->removeExtraKeys) {
|
||||||
$normalized[$name] = $val;
|
$normalized[$name] = $val;
|
||||||
|
@ -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.
|
* 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
|
* @return $this
|
||||||
*
|
*
|
||||||
|
@ -231,6 +231,25 @@ class ArrayNodeDefinitionTest extends TestCase
|
|||||||
$this->assertFalse($this->getField($node, 'normalizeKeys'));
|
$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()
|
public function testPrototypeVariable()
|
||||||
{
|
{
|
||||||
$node = new ArrayNodeDefinition('root');
|
$node = new ArrayNodeDefinition('root');
|
||||||
|
@ -16,8 +16,10 @@ use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
|
|||||||
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
|
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
|
||||||
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
|
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
|
||||||
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
|
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
|
||||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the main form extension, which loads the core functionality.
|
* Represents the main form extension, which loads the core functionality.
|
||||||
@ -28,11 +30,13 @@ class CoreExtension extends AbstractExtension
|
|||||||
{
|
{
|
||||||
private $propertyAccessor;
|
private $propertyAccessor;
|
||||||
private $choiceListFactory;
|
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->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
|
||||||
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
|
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
|
||||||
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function loadTypes()
|
protected function loadTypes()
|
||||||
@ -74,4 +78,11 @@ class CoreExtension extends AbstractExtension
|
|||||||
new Type\ColorType(),
|
new Type\ColorType(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function loadTypeExtensions()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
new TransformationFailureExtension($this->translator),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,16 +73,17 @@ class PropertyPathMapper implements DataMapperInterface
|
|||||||
// Write-back is disabled if the form is not synchronized (transformation failed),
|
// 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 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 (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
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the data is identical to the value in $data, we are
|
// If the data is identical to the value in $data, we are
|
||||||
// dealing with a reference
|
// dealing with a reference
|
||||||
if (!\is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
|
if (!\is_object($data) || !$config->getByReference() || $propertyValue !== $this->propertyAccessor->getValue($data, $propertyPath)) {
|
||||||
$this->propertyAccessor->setValue($data, $propertyPath, $form->getData());
|
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -357,4 +357,39 @@ class PropertyPathMapperTest extends TestCase
|
|||||||
|
|
||||||
$this->mapper->mapFormsToData(array($form), $car);
|
$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()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user