feature #37968 [Form] Add new way of mapping data using callback functions (yceruto)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[Form] Add new way of mapping data using callback functions

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | yes
| Tickets       | Fix #37597 (partially)
| License       | MIT
| Doc PR        | https://github.com/symfony/symfony-docs/pull/14241

Replaces https://github.com/symfony/symfony/pull/37614

## What this solves

Objects and Forms have different mechanisms for structuring data. When you build an object model with a lot of business logic it's valuable to use these mechanisms to better collect the data and the behavior that goes with it. Doing so leads to variant schemas; that is, the object model schema and the form schema don't match up.

You still need to transfer data between the two schemas, and this data transfer becomes a complexity in its own right. If the objects know about the form structure, changes in one tend to ripple to the other.

Currently, the Data Mapper layer separates the objects from the form, transfering data between the two and also isolating them from each other. That's fine, but at present the default data mapper has a limitation: _it's very tied to one property path_ (see [`PropertyPathMapper`](https://github.com/symfony/symfony/blob/5.1/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php)).

That said, you'll have to write your own data mapper in the following situations:
 * When the property path differs for reading and writing
 * When several form fields are mapped to a single method
 * When you need to read data based on the model's state
 * When the mapping of the model depends on the submitted form data
 * ...

Also, when we create a new data mapper, we usually forget about checking the status of the given data and forms. Whether the data is empty or not; throw an exception if the given data is not an object/array and whether the form field is submitted/synchronized/disabled or not. Not doing that could lead to unwanted behavior.

## What this proposes

Create a new way to write and read values to/from an object/array using callback functions. This feature would be tied to each form field and would also mean a new way of mapping data, but a very convenient one, in which it won't be necessary to define a new data mapper and take into account all what it would imply when you only need to map one field in a different manner or perhaps in only one direction (writing or reading the value).

This PR adds two new options for each form type: `getter` and `setter`, allowed to be `null` or `callable`:
```php
$builder->add('name', TextType::class, [
    'getter' => function (Person $person, FormInterface $form): string {
        return $person->firstName().' '.$person->lastName();
    },
    'setter' => function (Person &$person, ?string $name, FormInterface $form): void {
        $person->rename($name);
    },
]);
```
This would give us the same possibilities as data mappers, but within the form field scope, where:
 * `$person` is the view data, basically the underlying data to the form.
 * `$form` is the current child form that is being mapped.
 * `$name` is the submitted data that belongs to that field.

These two callbacks will be executed following the same rules as for property paths before read and write any value (e.i. early return if empty data, skip mapping if the form field is not mapped or it's disabled, etc).

## What this also proposes

I based the implementation on solving this problem first:

> https://github.com/symfony/symfony/pull/37614#issuecomment-662957865
> [...] the property_path option defines the rules on how it's accessed. From there, the actual way it is accessed (direct property access, accessors, reflection, whatever) are hidden from view. All that matters is the property path (which is deduced from the name if not explicitly set). [...]

So splitting the default data mapper `PropertyPathMapper` into two artifacts: "[DataMapper](https://github.com/yceruto/symfony/blob/data_accessor/src/Symfony/Component/Form/Extension/Core/DataMapper/DataMapper.php)" and "[DataAccessor](https://github.com/yceruto/symfony/blob/data_accessor/src/Symfony/Component/Form/DataAccessorInterface.php)" would allow us adding multiple data accessors along the way (the code that varies in this case) without having to reinvent the wheel over and over again (the data mapper code).

You can also think about a new `ReflectionAccessor` for instance? or use this `CallbackAccessor` to map your form partially from an external API? yes, you could do it :)

Here is a view of the proposed changes:

![data_accessor](https://user-images.githubusercontent.com/2028198/91452859-16bf8f00-e84d-11ea-8564-d497c2f73730.png)

Where "DataMapper" will take care of common checks, iterates the given child forms, manages the form data and all what is needed for mapping a standard form, whereas "DataAccessor" will take care of how to read and write values to/from the underlying object or array.

## BC

The `PropertyPathMapper` is being deprecated in favor of `DataMapper` class, which uses the `PropertyPathAccessor` by default.

Although `DataMapper` is now the default for each compound form, the behavior must remains the same (tests prove it). So that if `getter` or `setter` option is null (they're by default) the `CallbackAccessor` will falls back to `PropertyPathAccessor` either for reading or writing values.

---

Sorry for the long description, but I think that sometimes it is necessary for too complex issues and big changes. Besides, now you know a little more about what these changes is about.

/cc @xabbuh as creator of [rich-model-forms-bundle](https://github.com/sensiolabs-de/rich-model-forms-bundle) and @alcaeus as you had worked on this issue.

WDYT?

Commits
-------

878effaf47 add new way of mapping data using callback functions
This commit is contained in:
Fabien Potencier 2020-09-16 14:32:39 +02:00
commit c34865f175
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 used to be added by default to the seed, which is not the case anymore. This allows sharing caches between
apps or different environments. 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 Lock
---- ----

View File

@ -48,6 +48,7 @@ Form
* Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()`. * 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 `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. * The `NumberToLocalizedStringTransformer::ROUND_*` constants have been removed, use `\NumberFormatter::ROUND_*` instead.
* Removed `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`.
FrameworkBundle 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 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 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\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 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. * Maps arrays/objects to/from forms using property paths.
* *
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since symfony/form 5.2. Use {@see DataMapper} instead.
*/ */
class PropertyPathMapper implements DataMapperInterface class PropertyPathMapper implements DataMapperInterface
{ {

View File

@ -12,7 +12,10 @@
namespace Symfony\Component\Form\Extension\Core\Type; namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\Exception\LogicException; 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\Extension\Core\EventListener\TrimListener;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormConfigBuilderInterface; use Symfony\Component\Form\FormConfigBuilderInterface;
@ -25,11 +28,14 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class FormType extends BaseType class FormType extends BaseType
{ {
private $propertyAccessor; private $dataMapper;
public function __construct(PropertyAccessorInterface $propertyAccessor = null) 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']) ->setCompound($options['compound'])
->setData($isDataOptionSet ? $options['data'] : null) ->setData($isDataOptionSet ? $options['data'] : null)
->setDataLocked($isDataOptionSet) ->setDataLocked($isDataOptionSet)
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null) ->setDataMapper($options['compound'] ? $this->dataMapper : null)
->setMethod($options['method']) ->setMethod($options['method'])
->setAction($options['action']); ->setAction($options['action']);
@ -202,6 +208,8 @@ class FormType extends BaseType
'invalid_message' => 'This value is not valid.', 'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => [], 'invalid_message_parameters' => [],
'is_empty_callback' => null, 'is_empty_callback' => null,
'getter' => null,
'setter' => null,
]); ]);
$resolver->setAllowedTypes('label_attr', 'array'); $resolver->setAllowedTypes('label_attr', 'array');
@ -211,6 +219,11 @@ class FormType extends BaseType
$resolver->setAllowedTypes('help_attr', 'array'); $resolver->setAllowedTypes('help_attr', 'array');
$resolver->setAllowedTypes('help_html', 'bool'); $resolver->setAllowedTypes('help_html', 'bool');
$resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']); $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 PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher; 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\Form;
use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
@ -409,7 +409,7 @@ abstract class AbstractRequestHandlerTest extends TestCase
$builder->setCompound($compound); $builder->setCompound($compound);
if ($compound) { if ($compound) {
$builder->setDataMapper(new PropertyPathMapper()); $builder->setDataMapper(new DataMapper());
} }
return $builder; return $builder;

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Form\Tests; namespace Symfony\Component\Form\Tests;
use Symfony\Component\EventDispatcher\EventDispatcher; 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\Extension\HttpFoundation\HttpFoundationRequestHandler;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
@ -394,17 +394,17 @@ class CompoundFormTest extends AbstractFormTest
{ {
$form = $this->getBuilder() $form = $this->getBuilder()
->setCompound(true) ->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 // is currently contained in InheritDataAwareIterator, but even
// if that changes, this test should still function. // if that changes, this test should still function.
->setDataMapper(new PropertyPathMapper()) ->setDataMapper(new DataMapper())
->getForm(); ->getForm();
$childToBeRemoved = $this->createForm('removed', false); $childToBeRemoved = $this->createForm('removed', false);
$childToBeAdded = $this->createForm('added', false); $childToBeAdded = $this->createForm('added', false);
$child = $this->getBuilder('child', new EventDispatcher()) $child = $this->getBuilder('child', new EventDispatcher())
->setCompound(true) ->setCompound(true)
->setDataMapper(new PropertyPathMapper()) ->setDataMapper(new DataMapper())
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($form, $childToBeAdded) { ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($form, $childToBeAdded) {
$form->remove('removed'); $form->remove('removed');
$form->add($childToBeAdded); $form->add($childToBeAdded);
@ -449,7 +449,7 @@ class CompoundFormTest extends AbstractFormTest
public function testSetDataDoesNotMapViewDataToChildrenWithLockedSetData() public function testSetDataDoesNotMapViewDataToChildrenWithLockedSetData()
{ {
$mapper = new PropertyPathMapper(); $mapper = new DataMapper();
$viewData = [ $viewData = [
'firstName' => 'Fabien', 'firstName' => 'Fabien',
'lastName' => 'Pot', '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\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\PropertyAccess\PropertyPath;
/**
* @group legacy
*/
class PropertyPathMapperTest extends TestCase 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 Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher; 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\EventListener\ResizeFormListener;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormBuilder;
@ -31,7 +31,7 @@ class ResizeFormListenerTest extends TestCase
$this->factory = (new FormFactoryBuilder())->getFormFactory(); $this->factory = (new FormFactoryBuilder())->getFormFactory();
$this->form = $this->getBuilder() $this->form = $this->getBuilder()
->setCompound(true) ->setCompound(true)
->setDataMapper(new PropertyPathMapper()) ->setDataMapper(new DataMapper())
->getForm(); ->getForm();
} }
@ -268,12 +268,12 @@ class ResizeFormListenerTest extends TestCase
$this->form->setData(['0' => ['name' => 'John'], '1' => ['name' => 'Jane']]); $this->form->setData(['0' => ['name' => 'John'], '1' => ['name' => 'Jane']]);
$form1 = $this->getBuilder('0') $form1 = $this->getBuilder('0')
->setCompound(true) ->setCompound(true)
->setDataMapper(new PropertyPathMapper()) ->setDataMapper(new DataMapper())
->getForm(); ->getForm();
$form1->add($this->getForm('name')); $form1->add($this->getForm('name'));
$form2 = $this->getBuilder('1') $form2 = $this->getBuilder('1')
->setCompound(true) ->setCompound(true)
->setDataMapper(new PropertyPathMapper()) ->setDataMapper(new DataMapper())
->getForm(); ->getForm();
$form2->add($this->getForm('name')); $form2->add($this->getForm('name'));
$this->form->add($form1); $this->form->add($form1);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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