Skip to content

Commit c58671f

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 c58671f

File tree

16 files changed

+804
-27
lines changed

16 files changed

+804
-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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
interface DataAccessorInterface
18+
{
19+
/**
20+
* Returns the value at the end of the object graph.
21+
*
22+
* @param object|array $objectOrArray The object or array to traverse
23+
* @param FormInterface $form The {@link FormInterface()} instance to check
24+
*/
25+
public function getValue($objectOrArray, FormInterface $form);
26+
27+
/**
28+
* Sets the value at the end of the object graph.
29+
*
30+
* @param object|array $objectOrArray The object or array to modify
31+
* @param mixed $value The value to set at the end of the object graph
32+
* @param FormInterface $form The {@link FormInterface()} instance to check
33+
*/
34+
public function setValue(&$objectOrArray, $value, FormInterface $form): void;
35+
36+
/**
37+
* Returns whether a value can be read from an object graph.
38+
*
39+
* Whenever this method returns true, {@link getValue()} is guaranteed not
40+
* to throw an exception when called with the same arguments.
41+
*
42+
* @param object|array $objectOrArray The object or array to check
43+
* @param FormInterface $form The {@link FormInterface()} instance to check
44+
*
45+
* @return bool Whether the value can be read
46+
*/
47+
public function isReadable($objectOrArray, FormInterface $form): bool;
48+
49+
/**
50+
* Returns whether a value can be written at a given object graph.
51+
*
52+
* Whenever this method returns true, {@link setValue()} is guaranteed not
53+
* to throw an exception when called with the same arguments.
54+
*
55+
* @param object|array $objectOrArray The object or array to check
56+
* @param FormInterface $form The {@link FormInterface()} instance to check
57+
*
58+
* @return bool Whether the value can be set
59+
*/
60+
public function isWritable($objectOrArray, FormInterface $form): bool;
61+
}
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 $decorated;
25+
26+
public function __construct(DataAccessorInterface $decorated)
27+
{
28+
$this->decorated = $decorated;
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->decorated->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->decorated->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->decorated->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->decorated->isWritable($data, $form);
77+
}
78+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
31+
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
32+
{
33+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function getValue($data, FormInterface $form)
40+
{
41+
return $this->getPropertyValue($data, $form->getPropertyPath());
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
public function setValue(&$data, $propertyValue, FormInterface $form): void
48+
{
49+
$propertyPath = $form->getPropertyPath();
50+
51+
// If the field is of type DateTimeInterface and the data is the same skip the update to
52+
// keep the original object hash
53+
if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->getPropertyValue($data, $propertyPath)) {
54+
return;
55+
}
56+
57+
// If the data is identical to the value in $data, we are
58+
// dealing with a reference
59+
if (!\is_object($data) || !$form->getConfig()->getByReference() || $propertyValue !== $this->getPropertyValue($data, $propertyPath)) {
60+
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
61+
}
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*/
67+
public function isReadable($data, FormInterface $form): bool
68+
{
69+
return null !== $form->getPropertyPath();
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function isWritable($data, FormInterface $form): bool
76+
{
77+
return null !== $form->getPropertyPath();
78+
}
79+
80+
private function getPropertyValue($data, $propertyPath)
81+
{
82+
try {
83+
return $this->propertyAccessor->getValue($data, $propertyPath);
84+
} catch (AccessException $e) {
85+
if (!$e instanceof UninitializedPropertyException
86+
// For versions without UninitializedPropertyException check the exception message
87+
&& (class_exists(UninitializedPropertyException::class) || false === strpos($e->getMessage(), 'You should initialize it'))
88+
) {
89+
throw $e;
90+
}
91+
92+
return null;
93+
}
94+
}
95+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
class DataMapper implements DataMapperInterface
24+
{
25+
private $dataAccessor;
26+
27+
public function __construct(DataAccessorInterface $dataAccessor = null)
28+
{
29+
$this->dataAccessor = $dataAccessor ?? new CallbackAccessor(new PropertyPathAccessor());
30+
}
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public function mapDataToForms($data, iterable $forms): void
36+
{
37+
$empty = null === $data || [] === $data;
38+
39+
if (!$empty && !\is_array($data) && !\is_object($data)) {
40+
throw new UnexpectedTypeException($data, 'object, array or empty');
41+
}
42+
43+
foreach ($forms as $form) {
44+
$config = $form->getConfig();
45+
46+
if (!$empty && $this->dataAccessor->isReadable($data, $form) && $config->getMapped()) {
47+
$form->setData($this->dataAccessor->getValue($data, $form));
48+
} else {
49+
$form->setData($config->getData());
50+
}
51+
}
52+
}
53+
54+
/**
55+
* {@inheritdoc}
56+
*/
57+
public function mapFormsToData(iterable $forms, &$data): void
58+
{
59+
if (null === $data) {
60+
return;
61+
}
62+
63+
if (!\is_array($data) && !\is_object($data)) {
64+
throw new UnexpectedTypeException($data, 'object, array or empty');
65+
}
66+
67+
foreach ($forms as $form) {
68+
$config = $form->getConfig();
69+
70+
// Write-back is disabled if the form is not synchronized (transformation failed),
71+
// if the form was not submitted and if the form is disabled (modification not allowed)
72+
if ($this->dataAccessor->isWritable($data, $form) && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) {
73+
$this->dataAccessor->setValue($data, $form->getData(), $form);
74+
}
75+
}
76+
}
77+
}

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)