add new way of mapping data using callback functions

This commit is contained in:
Yonel Ceruto 2020-08-27 06:09:23 -04:00
parent 845c232dd5
commit 878effaf47
25 changed files with 947 additions and 59 deletions

View File

@ -16,6 +16,28 @@ FrameworkBundle
used to be added by default to the seed, which is not the case anymore. This allows sharing caches between
apps or different environments.
Form
----
* Deprecated `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`.
Before:
```php
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
$builder->setDataMapper(new PropertyPathMapper());
```
After:
```php
use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
$builder->setDataMapper(new DataMapper(new PropertyPathAccessor()));
```
Lock
----

View File

@ -48,6 +48,7 @@ Form
* Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()`.
* The `Symfony\Component\Form\Extension\Validator\Util\ServerParams` class has been removed, use its parent `Symfony\Component\Form\Util\ServerParams` instead.
* The `NumberToLocalizedStringTransformer::ROUND_*` constants have been removed, use `\NumberFormatter::ROUND_*` instead.
* Removed `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`.
FrameworkBundle
---------------

View File

@ -5,6 +5,8 @@ CHANGELOG
-----
* Added support for using the `{{ label }}` placeholder in constraint messages, which is replaced in the `ViolationMapper` by the corresponding field form label.
* Added `DataMapper`, `ChainAccessor`, `PropertyPathAccessor` and `CallbackAccessor` with new callable `getter` and `setter` options for each form type
* Deprecated `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`
5.1.0
-----

View File

@ -0,0 +1,69 @@
<?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;
/**
* Writes and reads values to/from an object or array bound to a form.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
interface DataAccessorInterface
{
/**
* Returns the value at the end of the property of the object graph.
*
* @param object|array $viewData The view data of the compound form
* @param FormInterface $form The {@link FormInterface()} instance to check
*
* @return mixed The value at the end of the property
*
* @throws Exception\AccessException If unable to read from the given form data
*/
public function getValue($viewData, FormInterface $form);
/**
* Sets the value at the end of the property of the object graph.
*
* @param object|array $viewData The view data of the compound form
* @param mixed $value The value to set at the end of the object graph
* @param FormInterface $form The {@link FormInterface()} instance to check
*
* @throws Exception\AccessException If unable to write the given value
*/
public function setValue(&$viewData, $value, FormInterface $form): void;
/**
* Returns whether a value can be read from an object graph.
*
* Whenever this method returns true, {@link getValue()} is guaranteed not
* to throw an exception when called with the same arguments.
*
* @param object|array $viewData The view data of the compound form
* @param FormInterface $form The {@link FormInterface()} instance to check
*
* @return bool Whether the value can be read
*/
public function isReadable($viewData, FormInterface $form): bool;
/**
* Returns whether a value can be written at a given object graph.
*
* Whenever this method returns true, {@link setValue()} is guaranteed not
* to throw an exception when called with the same arguments.
*
* @param object|array $viewData The view data of the compound form
* @param FormInterface $form The {@link FormInterface()} instance to check
*
* @return bool Whether the value can be set
*/
public function isWritable($viewData, FormInterface $form): bool;
}

View File

@ -0,0 +1,16 @@
<?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\Exception;
class AccessException extends RuntimeException
{
}

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\DataAccessor;
use Symfony\Component\Form\DataAccessorInterface;
use Symfony\Component\Form\Exception\AccessException;
use Symfony\Component\Form\FormInterface;
/**
* Writes and reads values to/from an object or array using callback functions.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class CallbackAccessor implements DataAccessorInterface
{
/**
* {@inheritdoc}
*/
public function getValue($data, FormInterface $form)
{
if (null === $getter = $form->getConfig()->getOption('getter')) {
throw new AccessException('Unable to read from the given form data as no getter is defined.');
}
return ($getter)($data, $form);
}
/**
* {@inheritdoc}
*/
public function setValue(&$data, $value, FormInterface $form): void
{
if (null === $setter = $form->getConfig()->getOption('setter')) {
throw new AccessException('Unable to write the given value as no setter is defined.');
}
($setter)($data, $form->getData(), $form);
}
/**
* {@inheritdoc}
*/
public function isReadable($data, FormInterface $form): bool
{
return null !== $form->getConfig()->getOption('getter');
}
/**
* {@inheritdoc}
*/
public function isWritable($data, FormInterface $form): bool
{
return null !== $form->getConfig()->getOption('setter');
}
}

View File

@ -0,0 +1,90 @@
<?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\DataAccessor;
use Symfony\Component\Form\DataAccessorInterface;
use Symfony\Component\Form\Exception\AccessException;
use Symfony\Component\Form\FormInterface;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class ChainAccessor implements DataAccessorInterface
{
private $accessors;
/**
* @param DataAccessorInterface[]|iterable $accessors
*/
public function __construct(iterable $accessors)
{
$this->accessors = $accessors;
}
/**
* {@inheritdoc}
*/
public function getValue($data, FormInterface $form)
{
foreach ($this->accessors as $accessor) {
if ($accessor->isReadable($data, $form)) {
return $accessor->getValue($data, $form);
}
}
throw new AccessException('Unable to read from the given form data as no accessor in the chain is able to read the data.');
}
/**
* {@inheritdoc}
*/
public function setValue(&$data, $value, FormInterface $form): void
{
foreach ($this->accessors as $accessor) {
if ($accessor->isWritable($data, $form)) {
$accessor->setValue($data, $value, $form);
return;
}
}
throw new AccessException('Unable to write the given value as no accessor in the chain is able to set the data.');
}
/**
* {@inheritdoc}
*/
public function isReadable($data, FormInterface $form): bool
{
foreach ($this->accessors as $accessor) {
if ($accessor->isReadable($data, $form)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function isWritable($data, FormInterface $form): bool
{
foreach ($this->accessors as $accessor) {
if ($accessor->isWritable($data, $form)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,102 @@
<?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\DataAccessor;
use Symfony\Component\Form\DataAccessorInterface;
use Symfony\Component\Form\Exception\AccessException;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Writes and reads values to/from an object or array using property path.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathAccessor implements DataAccessorInterface
{
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
}
/**
* {@inheritdoc}
*/
public function getValue($data, FormInterface $form)
{
if (null === $propertyPath = $form->getPropertyPath()) {
throw new AccessException('Unable to read from the given form data as no property path is defined.');
}
return $this->getPropertyValue($data, $propertyPath);
}
/**
* {@inheritdoc}
*/
public function setValue(&$data, $propertyValue, FormInterface $form): void
{
if (null === $propertyPath = $form->getPropertyPath()) {
throw new AccessException('Unable to write the given value as no property path is defined.');
}
// If the field is of type DateTimeInterface and the data is the same skip the update to
// keep the original object hash
if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->getPropertyValue($data, $propertyPath)) {
return;
}
// If the data is identical to the value in $data, we are
// dealing with a reference
if (!\is_object($data) || !$form->getConfig()->getByReference() || $propertyValue !== $this->getPropertyValue($data, $propertyPath)) {
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
}
}
/**
* {@inheritdoc}
*/
public function isReadable($data, FormInterface $form): bool
{
return null !== $form->getPropertyPath();
}
/**
* {@inheritdoc}
*/
public function isWritable($data, FormInterface $form): bool
{
return null !== $form->getPropertyPath();
}
private function getPropertyValue($data, $propertyPath)
{
try {
return $this->propertyAccessor->getValue($data, $propertyPath);
} catch (PropertyAccessException $e) {
if (!$e instanceof UninitializedPropertyException
// For versions without UninitializedPropertyException check the exception message
&& (class_exists(UninitializedPropertyException::class) || false === strpos($e->getMessage(), 'You should initialize it'))
) {
throw $e;
}
return null;
}
}
}

View File

@ -0,0 +1,83 @@
<?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\DataMapper;
use Symfony\Component\Form\DataAccessorInterface;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
/**
* Maps arrays/objects to/from forms using data accessors.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DataMapper implements DataMapperInterface
{
private $dataAccessor;
public function __construct(DataAccessorInterface $dataAccessor = null)
{
$this->dataAccessor = $dataAccessor ?? new ChainAccessor([
new CallbackAccessor(),
new PropertyPathAccessor(),
]);
}
/**
* {@inheritdoc}
*/
public function mapDataToForms($data, iterable $forms): void
{
$empty = null === $data || [] === $data;
if (!$empty && !\is_array($data) && !\is_object($data)) {
throw new UnexpectedTypeException($data, 'object, array or empty');
}
foreach ($forms as $form) {
$config = $form->getConfig();
if (!$empty && $config->getMapped() && $this->dataAccessor->isReadable($data, $form)) {
$form->setData($this->dataAccessor->getValue($data, $form));
} else {
$form->setData($config->getData());
}
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData(iterable $forms, &$data): void
{
if (null === $data) {
return;
}
if (!\is_array($data) && !\is_object($data)) {
throw new UnexpectedTypeException($data, 'object, array or empty');
}
foreach ($forms as $form) {
$config = $form->getConfig();
// 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 ($config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled() && $this->dataAccessor->isWritable($data, $form)) {
$this->dataAccessor->setValue($data, $form->getData(), $form);
}
}
}
}

View File

@ -18,10 +18,14 @@ use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
trigger_deprecation('symfony/form', '5.2', 'The "%s" class is deprecated. Use "%s" instead.', PropertyPathMapper::class, DataMapper::class);
/**
* Maps arrays/objects to/from forms using property paths.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since symfony/form 5.2. Use {@see DataMapper} instead.
*/
class PropertyPathMapper implements DataMapperInterface
{

View File

@ -12,7 +12,10 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormConfigBuilderInterface;
@ -25,11 +28,14 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class FormType extends BaseType
{
private $propertyAccessor;
private $dataMapper;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->dataMapper = new DataMapper(new ChainAccessor([
new CallbackAccessor(),
new PropertyPathAccessor($propertyAccessor ?? PropertyAccess::createPropertyAccessor()),
]));
}
/**
@ -52,7 +58,7 @@ class FormType extends BaseType
->setCompound($options['compound'])
->setData($isDataOptionSet ? $options['data'] : null)
->setDataLocked($isDataOptionSet)
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)
->setDataMapper($options['compound'] ? $this->dataMapper : null)
->setMethod($options['method'])
->setAction($options['action']);
@ -202,6 +208,8 @@ class FormType extends BaseType
'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => [],
'is_empty_callback' => null,
'getter' => null,
'setter' => null,
]);
$resolver->setAllowedTypes('label_attr', 'array');
@ -211,6 +219,11 @@ class FormType extends BaseType
$resolver->setAllowedTypes('help_attr', 'array');
$resolver->setAllowedTypes('help_html', 'bool');
$resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']);
$resolver->setAllowedTypes('getter', ['null', 'callable']);
$resolver->setAllowedTypes('setter', ['null', 'callable']);
$resolver->setInfo('getter', 'A callable that accepts two arguments (the view data and the current form field) and must return a value.');
$resolver->setInfo('setter', 'A callable that accepts three arguments (a reference to the view data, the submitted value and the current form field).');
}
/**

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError;
@ -409,7 +409,7 @@ abstract class AbstractRequestHandlerTest extends TestCase
$builder->setCompound($compound);
if ($compound) {
$builder->setDataMapper(new PropertyPathMapper());
$builder->setDataMapper(new DataMapper());
}
return $builder;

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Form\Tests;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
@ -394,17 +394,17 @@ class CompoundFormTest extends AbstractFormTest
{
$form = $this->getBuilder()
->setCompound(true)
// We test using PropertyPathMapper on purpose. The traversal logic
// We test using DataMapper on purpose. The traversal logic
// is currently contained in InheritDataAwareIterator, but even
// if that changes, this test should still function.
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$childToBeRemoved = $this->createForm('removed', false);
$childToBeAdded = $this->createForm('added', false);
$child = $this->getBuilder('child', new EventDispatcher())
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($form, $childToBeAdded) {
$form->remove('removed');
$form->add($childToBeAdded);
@ -449,7 +449,7 @@ class CompoundFormTest extends AbstractFormTest
public function testSetDataDoesNotMapViewDataToChildrenWithLockedSetData()
{
$mapper = new PropertyPathMapper();
$mapper = new DataMapper();
$viewData = [
'firstName' => 'Fabien',
'lastName' => 'Pot',

View File

@ -0,0 +1,427 @@
<?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\DataMapper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigBuilder;
use Symfony\Component\Form\Tests\Fixtures\TypehintedPropertiesCar;
use Symfony\Component\PropertyAccess\PropertyPath;
class DataMapperTest extends TestCase
{
/**
* @var DataMapper
*/
private $mapper;
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
protected function setUp(): void
{
$this->mapper = new DataMapper();
$this->dispatcher = new EventDispatcher();
}
public function testMapDataToFormsPassesObjectRefIfByReference()
{
$car = new \stdClass();
$engine = new \stdClass();
$car->engine = $engine;
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$form = new Form($config);
$this->mapper->mapDataToForms($car, [$form]);
self::assertSame($engine, $form->getData());
}
public function testMapDataToFormsPassesObjectCloneIfNotByReference()
{
$car = new \stdClass();
$engine = new \stdClass();
$engine->brand = 'Rolls-Royce';
$car->engine = $engine;
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(false);
$config->setPropertyPath($propertyPath);
$form = new Form($config);
$this->mapper->mapDataToForms($car, [$form]);
self::assertNotSame($engine, $form->getData());
self::assertEquals($engine, $form->getData());
}
public function testMapDataToFormsIgnoresEmptyPropertyPath()
{
$car = new \stdClass();
$config = new FormConfigBuilder(null, \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$form = new Form($config);
self::assertNull($form->getPropertyPath());
$this->mapper->mapDataToForms($car, [$form]);
self::assertNull($form->getData());
}
public function testMapDataToFormsIgnoresUnmapped()
{
$car = new \stdClass();
$car->engine = new \stdClass();
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setMapped(false);
$config->setPropertyPath($propertyPath);
$form = new Form($config);
$this->mapper->mapDataToForms($car, [$form]);
self::assertNull($form->getData());
}
/**
* @requires PHP 7.4
*/
public function testMapDataToFormsIgnoresUninitializedProperties()
{
$engineForm = new Form(new FormConfigBuilder('engine', null, $this->dispatcher));
$colorForm = new Form(new FormConfigBuilder('color', null, $this->dispatcher));
$car = new TypehintedPropertiesCar();
$car->engine = 'BMW';
$this->mapper->mapDataToForms($car, [$engineForm, $colorForm]);
self::assertSame($car->engine, $engineForm->getData());
self::assertNull($colorForm->getData());
}
public function testMapDataToFormsSetsDefaultDataIfPassedDataIsNull()
{
$default = new \stdClass();
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData($default);
$form = new Form($config);
$this->mapper->mapDataToForms(null, [$form]);
self::assertSame($default, $form->getData());
}
public function testMapDataToFormsSetsDefaultDataIfPassedDataIsEmptyArray()
{
$default = new \stdClass();
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData($default);
$form = new Form($config);
$this->mapper->mapDataToForms([], [$form]);
self::assertSame($default, $form->getData());
}
public function testMapFormsToDataWritesBackIfNotByReference()
{
$car = new \stdClass();
$car->engine = new \stdClass();
$engine = new \stdClass();
$engine->brand = 'Rolls-Royce';
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(false);
$config->setPropertyPath($propertyPath);
$config->setData($engine);
$form = new SubmittedForm($config);
$this->mapper->mapFormsToData([$form], $car);
self::assertEquals($engine, $car->engine);
self::assertNotSame($engine, $car->engine);
}
public function testMapFormsToDataWritesBackIfByReferenceButNoReference()
{
$car = new \stdClass();
$car->engine = new \stdClass();
$engine = new \stdClass();
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData($engine);
$form = new SubmittedForm($config);
$this->mapper->mapFormsToData([$form], $car);
self::assertSame($engine, $car->engine);
}
public function testMapFormsToDataWritesBackIfByReferenceAndReference()
{
$car = new \stdClass();
$car->engine = 'BMW';
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('engine', null, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData('Rolls-Royce');
$form = new SubmittedForm($config);
$car->engine = 'Rolls-Royce';
$this->mapper->mapFormsToData([$form], $car);
self::assertSame('Rolls-Royce', $car->engine);
}
public function testMapFormsToDataIgnoresUnmapped()
{
$initialEngine = new \stdClass();
$car = new \stdClass();
$car->engine = $initialEngine;
$engine = new \stdClass();
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData($engine);
$config->setMapped(false);
$form = new SubmittedForm($config);
$this->mapper->mapFormsToData([$form], $car);
self::assertSame($initialEngine, $car->engine);
}
public function testMapFormsToDataIgnoresUnsubmittedForms()
{
$initialEngine = new \stdClass();
$car = new \stdClass();
$car->engine = $initialEngine;
$engine = new \stdClass();
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData($engine);
$form = new Form($config);
$this->mapper->mapFormsToData([$form], $car);
self::assertSame($initialEngine, $car->engine);
}
public function testMapFormsToDataIgnoresEmptyData()
{
$initialEngine = new \stdClass();
$car = new \stdClass();
$car->engine = $initialEngine;
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData(null);
$form = new Form($config);
$this->mapper->mapFormsToData([$form], $car);
self::assertSame($initialEngine, $car->engine);
}
public function testMapFormsToDataIgnoresUnsynchronized()
{
$initialEngine = new \stdClass();
$car = new \stdClass();
$car->engine = $initialEngine;
$engine = new \stdClass();
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData($engine);
$form = new NotSynchronizedForm($config);
$this->mapper->mapFormsToData([$form], $car);
self::assertSame($initialEngine, $car->engine);
}
public function testMapFormsToDataIgnoresDisabled()
{
$initialEngine = new \stdClass();
$car = new \stdClass();
$car->engine = $initialEngine;
$engine = new \stdClass();
$propertyPath = new PropertyPath('engine');
$config = new FormConfigBuilder('name', \stdClass::class, $this->dispatcher);
$config->setByReference(true);
$config->setPropertyPath($propertyPath);
$config->setData($engine);
$config->setDisabled(true);
$form = new SubmittedForm($config);
$this->mapper->mapFormsToData([$form], $car);
self::assertSame($initialEngine, $car->engine);
}
/**
* @requires PHP 7.4
*/
public function testMapFormsToUninitializedProperties()
{
$car = new TypehintedPropertiesCar();
$config = new FormConfigBuilder('engine', null, $this->dispatcher);
$config->setData('BMW');
$form = new SubmittedForm($config);
$this->mapper->mapFormsToData([$form], $car);
self::assertSame('BMW', $car->engine);
}
/**
* @dataProvider provideDate
*/
public function testMapFormsToDataDoesNotChangeEqualDateTimeInstance($date)
{
$article = [];
$publishedAt = $date;
$publishedAtValue = clone $publishedAt;
$article['publishedAt'] = $publishedAtValue;
$propertyPath = new PropertyPath('[publishedAt]');
$config = new FormConfigBuilder('publishedAt', \get_class($publishedAt), $this->dispatcher);
$config->setByReference(false);
$config->setPropertyPath($propertyPath);
$config->setData($publishedAt);
$form = new SubmittedForm($config);
$this->mapper->mapFormsToData([$form], $article);
self::assertSame($publishedAtValue, $article['publishedAt']);
}
public function provideDate(): array
{
return [
[new \DateTime()],
[new \DateTimeImmutable()],
];
}
public function testMapDataToFormsUsingGetCallbackOption()
{
$initialName = 'John Doe';
$person = new DummyPerson($initialName);
$config = new FormConfigBuilder('name', null, $this->dispatcher, [
'getter' => static function (DummyPerson $person) {
return $person->myName();
},
]);
$form = new Form($config);
$this->mapper->mapDataToForms($person, [$form]);
self::assertSame($initialName, $form->getData());
}
public function testMapFormsToDataUsingSetCallbackOption()
{
$person = new DummyPerson('John Doe');
$config = new FormConfigBuilder('name', null, $this->dispatcher, [
'setter' => static function (DummyPerson $person, $name) {
$person->rename($name);
},
]);
$config->setData('Jane Doe');
$form = new SubmittedForm($config);
$this->mapper->mapFormsToData([$form], $person);
self::assertSame('Jane Doe', $person->myName());
}
}
class SubmittedForm extends Form
{
public function isSubmitted(): bool
{
return true;
}
}
class NotSynchronizedForm extends SubmittedForm
{
public function isSynchronized(): bool
{
return false;
}
}
class DummyPerson
{
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function myName(): string
{
return $this->name;
}
public function rename($name): void
{
$this->name = $name;
}
}

View File

@ -22,6 +22,9 @@ use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* @group legacy
*/
class PropertyPathMapperTest extends TestCase
{
/**
@ -363,19 +366,3 @@ class PropertyPathMapperTest extends TestCase
];
}
}
class SubmittedForm extends Form
{
public function isSubmitted(): bool
{
return true;
}
}
class NotSynchronizedForm extends SubmittedForm
{
public function isSynchronized(): bool
{
return false;
}
}

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\EventListener;
use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilder;
@ -31,7 +31,7 @@ class ResizeFormListenerTest extends TestCase
$this->factory = (new FormFactoryBuilder())->getFormFactory();
$this->form = $this->getBuilder()
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
}
@ -268,12 +268,12 @@ class ResizeFormListenerTest extends TestCase
$this->form->setData(['0' => ['name' => 'John'], '1' => ['name' => 'Jane']]);
$form1 = $this->getBuilder('0')
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$form1->add($this->getForm('name'));
$form2 = $this->getBuilder('1')
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$form2->add($this->getForm('name'));
$this->form->add($form1);

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Tests\Extension\Csrf\EventListener;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormEvent;
@ -33,7 +33,7 @@ class CsrfValidationListenerTest extends TestCase
$this->factory = (new FormFactoryBuilder())->getFormFactory();
$this->tokenManager = new CsrfTokenManager();
$this->form = $this->getBuilder()
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
}

View File

@ -15,7 +15,7 @@ use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Extension\Core\CoreExtension;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -81,7 +81,7 @@ class FormDataCollectorTest extends TestCase
$this->dataCollector = new FormDataCollector($this->dataExtractor);
$this->dispatcher = new EventDispatcher();
$this->factory = new FormFactory(new FormRegistry([new CoreExtension()], new ResolvedFormTypeFactory()));
$this->dataMapper = new PropertyPathMapper();
$this->dataMapper = new DataMapper();
$this->form = $this->createForm('name');
$this->childForm = $this->createForm('child');
$this->view = new FormView();

View File

@ -15,7 +15,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Form\Extension\Validator\Constraints\FormValidator;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
@ -107,7 +107,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$parent = $this->getBuilder('parent')
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$options = [
'validation_groups' => ['group1', 'group2'],
@ -130,7 +130,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$parent = $this->getBuilder('parent', null)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$options = ['validation_groups' => ['group1', 'group2']];
$form = $this->getBuilder('name', '\stdClass', $options)->getForm();
@ -169,7 +169,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$parent = $this->getBuilder('parent', null)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$options = [
'validation_groups' => ['group1', 'group2'],
@ -196,7 +196,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
])
->setData($object)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$form->setData($object);
@ -243,7 +243,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
];
$form = $this->getBuilder('name', null, $formOptions)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$childOptions = ['constraints' => [new NotBlank()]];
$child = $this->getCompoundForm(new \stdClass(), $childOptions);
@ -470,7 +470,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$parent = $this->getBuilder('parent')
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$form = $this->getForm('name', '\stdClass', [
'validation_groups' => 'form_group',
@ -497,7 +497,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$parent = $this->getBuilder('parent')
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$form = $this->getCompoundForm($object, [
'validation_groups' => 'form_group',
@ -525,7 +525,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$parentOptions = ['validation_groups' => 'group'];
$parent = $this->getBuilder('parent', null, $parentOptions)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$formOptions = ['constraints' => [new Valid()]];
$form = $this->getCompoundForm($object, $formOptions);
@ -546,7 +546,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$parentOptions = ['validation_groups' => [$this, 'getValidationGroups']];
$parent = $this->getBuilder('parent', null, $parentOptions)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$formOptions = ['constraints' => [new Valid()]];
$form = $this->getCompoundForm($object, $formOptions);
@ -571,7 +571,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
];
$parent = $this->getBuilder('parent', null, $parentOptions)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$formOptions = ['constraints' => [new Valid()]];
$form = $this->getCompoundForm($object, $formOptions);
@ -618,7 +618,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
{
$form = $this->getBuilder('parent', null, ['extra_fields_message' => 'Extra!|Extras!'])
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->add($this->getBuilder('child'))
->getForm();
@ -643,7 +643,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
{
$form = $this->getBuilder('parent', null, ['extra_fields_message' => 'Extra!|Extras!!'])
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->add($this->getBuilder('child'))
->getForm();
@ -669,7 +669,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$form = $this
->getBuilder('parent', null, ['allow_extra_fields' => true])
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->add($this->getBuilder('child'))
->getForm();
@ -698,7 +698,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$form = $this
->getBuilder('form', null, ['constraints' => [new NotBlank(['groups' => ['foo']])]])
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
$form->submit([
'extra_data' => 'foo',
@ -739,7 +739,7 @@ class FormValidatorTest extends ConstraintValidatorTestCase
return $this->getBuilder('name', \is_object($data) ? \get_class($data) : null, $options)
->setData($data)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->setDataMapper(new DataMapper())
->getForm();
}

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\EventListener;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Validator\Constraints\Form as FormConstraint;
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
@ -79,7 +79,7 @@ class ValidationListenerTest extends TestCase
$config->setCompound($compound);
if ($compound) {
$config->setDataMapper(new PropertyPathMapper());
$config->setDataMapper(new DataMapper());
}
return new Form($config);

View File

@ -16,7 +16,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigBuilder;
@ -82,7 +82,7 @@ class ViolationMapperTest extends TestCase
$config->setInheritData($inheritData);
$config->setPropertyPath($propertyPath);
$config->setCompound(true);
$config->setDataMapper(new PropertyPathMapper());
$config->setDataMapper(new DataMapper());
if (!$synchronized) {
$config->addViewTransformer(new CallbackTransformer(
@ -1643,7 +1643,7 @@ class ViolationMapperTest extends TestCase
$config->setInheritData(false);
$config->setPropertyPath('name');
$config->setCompound(true);
$config->setDataMapper(new PropertyPathMapper());
$config->setDataMapper(new DataMapper());
$child = new Form($config);
$parent->add($child);
@ -1681,7 +1681,7 @@ class ViolationMapperTest extends TestCase
$config->setInheritData(false);
$config->setPropertyPath('custom');
$config->setCompound(true);
$config->setDataMapper(new PropertyPathMapper());
$config->setDataMapper(new DataMapper());
$child = new Form($config);
$parent->add($child);
@ -1719,7 +1719,7 @@ class ViolationMapperTest extends TestCase
$config->setInheritData(false);
$config->setPropertyPath('custom-id');
$config->setCompound(true);
$config->setDataMapper(new PropertyPathMapper());
$config->setDataMapper(new DataMapper());
$child = new Form($config);
$parent->add($child);

View File

@ -39,6 +39,7 @@
"by_reference",
"data",
"disabled",
"getter",
"help",
"help_attr",
"help_html",
@ -57,6 +58,7 @@
"property_path",
"required",
"row_attr",
"setter",
"translation_domain",
"upload_max_size_message"
]

View File

@ -17,7 +17,8 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice")
group_by by_reference
multiple data
placeholder disabled
preferred_choices help
preferred_choices getter
help
help_attr
help_html
help_translation_parameters
@ -35,6 +36,7 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice")
property_path
required
row_attr
setter
translation_domain
upload_max_size_message
--------------------------- -------------------- ------------------------------ -----------------------

View File

@ -17,6 +17,7 @@
"disabled",
"empty_data",
"error_bubbling",
"getter",
"help",
"help_attr",
"help_html",
@ -36,6 +37,7 @@
"property_path",
"required",
"row_attr",
"setter",
"translation_domain",
"trim",
"upload_max_size_message"

View File

@ -19,6 +19,7 @@ Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form")
disabled
empty_data
error_bubbling
getter
help
help_attr
help_html
@ -38,6 +39,7 @@ Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form")
property_path
required
row_attr
setter
translation_domain
trim
upload_max_size_message