Skip to content

Commit 43e2ff7

Browse files
committed
add new way to write and/or read values to/from an object or array using callback functions
1 parent bd26785 commit 43e2ff7

File tree

16 files changed

+826
-27
lines changed

16 files changed

+826
-27
lines changed

UPGRADE-5.2.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,28 @@ FrameworkBundle
1212
* Deprecated the public `form.factory`, `form.type.file`, `translator`, `security.csrf.token_manager`, `serializer`,
1313
`cache_clearer`, `filesystem` and `validator` services to private.
1414

15+
Form
16+
----
17+
18+
* Deprecated `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`.
19+
20+
Before:
21+
22+
```php
23+
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
24+
25+
$builder->setDataMapper(new PropertyPathMapper());
26+
```
27+
28+
After:
29+
30+
```php
31+
use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
32+
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
33+
34+
$builder->setDataMapper(new DataMapper(new PropertyPathAccessor()));
35+
```
36+
1537
Mime
1638
----
1739

UPGRADE-6.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Form
4848
* Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()`.
4949
* The `Symfony\Component\Form\Extension\Validator\Util\ServerParams` class has been removed, use its parent `Symfony\Component\Form\Util\ServerParams` instead.
5050
* The `NumberToLocalizedStringTransformer::ROUND_*` constants have been removed, use `\NumberFormatter::ROUND_*` instead.
51+
* Removed `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`.
5152

5253
FrameworkBundle
5354
---------------

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ CHANGELOG
44
5.2.0
55
-----
66

7-
* added `FormErrorNormalizer`
7+
* added `FormErrorNormalizer`
8+
* added `DataMapper`, `PropertyPathAccessor` and `CallbackAccessor` with new callable `get` and `set` options for each form type
9+
* deprecated `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`
810

911
5.1.0
1012
-----
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form;
13+
14+
/**
15+
* Writes and reads values to/from an object or array bound to a form.
16+
*
17+
* @author Yonel Ceruto <yonelceruto@gmail.com>
18+
*/
19+
interface DataAccessorInterface
20+
{
21+
/**
22+
* Returns the value at the end of the property of the object graph.
23+
*
24+
* @param object|array $objectOrArray The object or array to traverse
25+
* @param FormInterface $form The {@link FormInterface()} instance to check
26+
*/
27+
public function getValue($objectOrArray, FormInterface $form);
28+
29+
/**
30+
* Sets the value at the end of the property of the object graph.
31+
*
32+
* @param object|array $objectOrArray The object or array to modify
33+
* @param mixed $value The value to set at the end of the object graph
34+
* @param FormInterface $form The {@link FormInterface()} instance to check
35+
*/
36+
public function setValue(&$objectOrArray, $value, FormInterface $form): void;
37+
38+
/**
39+
* Returns whether a value can be read from an object graph.
40+
*
41+
* Whenever this method returns true, {@link getValue()} is guaranteed not
42+
* to throw an exception when called with the same arguments.
43+
*
44+
* @param object|array $objectOrArray The object or array to check
45+
* @param FormInterface $form The {@link FormInterface()} instance to check
46+
*
47+
* @return bool Whether the value can be read
48+
*/
49+
public function isReadable($objectOrArray, FormInterface $form): bool;
50+
51+
/**
52+
* Returns whether a value can be written at a given object graph.
53+
*
54+
* Whenever this method returns true, {@link setValue()} is guaranteed not
55+
* to throw an exception when called with the same arguments.
56+
*
57+
* @param object|array $objectOrArray The object or array to check
58+
* @param FormInterface $form The {@link FormInterface()} instance to check
59+
*
60+
* @return bool Whether the value can be set
61+
*/
62+
public function isWritable($objectOrArray, FormInterface $form): bool;
63+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\DataAccessor;
13+
14+
use Symfony\Component\Form\DataAccessorInterface;
15+
use Symfony\Component\Form\FormInterface;
16+
17+
/**
18+
* Writes and reads values to/from an object or array using callback functions.
19+
*
20+
* @author Yonel Ceruto <yonelceruto@gmail.com>
21+
*/
22+
class CallbackAccessor implements DataAccessorInterface
23+
{
24+
private $fallbackAccessor;
25+
26+
public function __construct(DataAccessorInterface $fallbackAccessor)
27+
{
28+
$this->fallbackAccessor = $fallbackAccessor;
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function getValue($data, FormInterface $form)
35+
{
36+
if (null !== $getter = $form->getConfig()->getOption('get')) {
37+
return ($getter)($data, $form);
38+
}
39+
40+
return $this->fallbackAccessor->getValue($data, $form);
41+
}
42+
43+
/**
44+
* {@inheritdoc}
45+
*/
46+
public function setValue(&$data, $value, FormInterface $form): void
47+
{
48+
if (null !== $setter = $form->getConfig()->getOption('set')) {
49+
($setter)($data, $form->getData(), $form);
50+
} else {
51+
$this->fallbackAccessor->setValue($data, $value, $form);
52+
}
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public function isReadable($data, FormInterface $form): bool
59+
{
60+
if (null !== $form->getConfig()->getOption('get')) {
61+
return true;
62+
}
63+
64+
return $this->fallbackAccessor->isReadable($data, $form);
65+
}
66+
67+
/**
68+
* {@inheritdoc}
69+
*/
70+
public function isWritable($data, FormInterface $form): bool
71+
{
72+
if (null !== $form->getConfig()->getOption('set')) {
73+
return true;
74+
}
75+
76+
return $this->fallbackAccessor->isWritable($data, $form);
77+
}
78+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\DataAccessor;
13+
14+
use Symfony\Component\Form\DataAccessorInterface;
15+
use Symfony\Component\Form\FormInterface;
16+
use Symfony\Component\PropertyAccess\Exception\AccessException;
17+
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
18+
use Symfony\Component\PropertyAccess\PropertyAccess;
19+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
20+
21+
/**
22+
* Writes and reads values to/from an object or array using property path.
23+
*
24+
* @author Yonel Ceruto <yonelceruto@gmail.com>
25+
* @author Bernhard Schussek <bschussek@gmail.com>
26+
*/
27+
class PropertyPathAccessor implements DataAccessorInterface
28+
{
29+
private $propertyAccessor;
30+
private $fallbackAccessor;
31+
32+
public function __construct(PropertyAccessorInterface $propertyAccessor = null, DataAccessorInterface $fallbackAccessor = null)
33+
{
34+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
35+
$this->fallbackAccessor = $fallbackAccessor;
36+
}
37+
38+
/**
39+
* {@inheritdoc}
40+
*/
41+
public function getValue($data, FormInterface $form)
42+
{
43+
return $this->getPropertyValue($data, $form->getPropertyPath());
44+
}
45+
46+
/**
47+
* {@inheritdoc}
48+
*/
49+
public function setValue(&$data, $propertyValue, FormInterface $form): void
50+
{
51+
$propertyPath = $form->getPropertyPath();
52+
53+
// If the field is of type DateTimeInterface and the data is the same skip the update to
54+
// keep the original object hash
55+
if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->getPropertyValue($data, $propertyPath)) {
56+
return;
57+
}
58+
59+
// If the data is identical to the value in $data, we are
60+
// dealing with a reference
61+
if (!\is_object($data) || !$form->getConfig()->getByReference() || $propertyValue !== $this->getPropertyValue($data, $propertyPath)) {
62+
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
63+
}
64+
}
65+
66+
/**
67+
* {@inheritdoc}
68+
*/
69+
public function isReadable($data, FormInterface $form): bool
70+
{
71+
if (null !== $form->getPropertyPath()) {
72+
return true;
73+
}
74+
75+
if (null === $this->fallbackAccessor) {
76+
return false;
77+
}
78+
79+
return $this->fallbackAccessor->isReadable($data, $form);
80+
}
81+
82+
/**
83+
* {@inheritdoc}
84+
*/
85+
public function isWritable($data, FormInterface $form): bool
86+
{
87+
if (null !== $form->getPropertyPath()) {
88+
return true;
89+
}
90+
91+
if (null === $this->fallbackAccessor) {
92+
return false;
93+
}
94+
95+
return $this->fallbackAccessor->isWritable($data, $form);
96+
}
97+
98+
private function getPropertyValue($data, $propertyPath)
99+
{
100+
try {
101+
return $this->propertyAccessor->getValue($data, $propertyPath);
102+
} catch (AccessException $e) {
103+
if (!$e instanceof UninitializedPropertyException
104+
// For versions without UninitializedPropertyException check the exception message
105+
&& (class_exists(UninitializedPropertyException::class) || false === strpos($e->getMessage(), 'You should initialize it'))
106+
) {
107+
throw $e;
108+
}
109+
110+
return null;
111+
}
112+
}
113+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\DataMapper;
13+
14+
use Symfony\Component\Form\DataAccessorInterface;
15+
use Symfony\Component\Form\DataMapperInterface;
16+
use Symfony\Component\Form\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
18+
use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
19+
20+
/**
21+
* Maps arrays/objects to/from forms using data accessors.
22+
*
23+
* @author Bernhard Schussek <bschussek@gmail.com>
24+
*/
25+
class DataMapper implements DataMapperInterface
26+
{
27+
private $dataAccessor;
28+
29+
public function __construct(DataAccessorInterface $dataAccessor = null)
30+
{
31+
$this->dataAccessor = $dataAccessor ?? new CallbackAccessor(new PropertyPathAccessor());
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function mapDataToForms($data, iterable $forms): void
38+
{
39+
$empty = null === $data || [] === $data;
40+
41+
if (!$empty && !\is_array($data) && !\is_object($data)) {
42+
throw new UnexpectedTypeException($data, 'object, array or empty');
43+
}
44+
45+
foreach ($forms as $form) {
46+
$config = $form->getConfig();
47+
48+
if (!$empty && $this->dataAccessor->isReadable($data, $form) && $config->getMapped()) {
49+
$form->setData($this->dataAccessor->getValue($data, $form));
50+
} else {
51+
$form->setData($config->getData());
52+
}
53+
}
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function mapFormsToData(iterable $forms, &$data): void
60+
{
61+
if (null === $data) {
62+
return;
63+
}
64+
65+
if (!\is_array($data) && !\is_object($data)) {
66+
throw new UnexpectedTypeException($data, 'object, array or empty');
67+
}
68+
69+
foreach ($forms as $form) {
70+
$config = $form->getConfig();
71+
72+
// Write-back is disabled if the form is not synchronized (transformation failed),
73+
// if the form was not submitted and if the form is disabled (modification not allowed)
74+
if ($this->dataAccessor->isWritable($data, $form) && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) {
75+
$this->dataAccessor->setValue($data, $form->getData(), $form);
76+
}
77+
}
78+
}
79+
}

src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
use Symfony\Component\PropertyAccess\PropertyAccess;
1919
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
2020

21+
trigger_deprecation('symfony/form', '5.2', 'The "%s" class is deprecated.', PropertyPathMapper::class);
22+
2123
/**
2224
* Maps arrays/objects to/from forms using property paths.
2325
*
2426
* @author Bernhard Schussek <bschussek@gmail.com>
27+
*
28+
* @deprecated since symfony/form 5.2
2529
*/
2630
class PropertyPathMapper implements DataMapperInterface
2731
{

0 commit comments

Comments
 (0)