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:
commit
c34865f175
@ -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
|
||||
----
|
||||
|
||||
|
@ -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
|
||||
---------------
|
||||
|
@ -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
|
||||
-----
|
||||
|
69
src/Symfony/Component/Form/DataAccessorInterface.php
Normal file
69
src/Symfony/Component/Form/DataAccessorInterface.php
Normal 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;
|
||||
}
|
16
src/Symfony/Component/Form/Exception/AccessException.php
Normal file
16
src/Symfony/Component/Form/Exception/AccessException.php
Normal 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
|
||||
{
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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).');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
]
|
||||
|
@ -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
|
||||
--------------------------- -------------------- ------------------------------ -----------------------
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user