Skip to content

Commit 878effa

Browse files
committed
add new way of mapping data using callback functions
1 parent 845c232 commit 878effa

25 files changed

+947
-59
lines changed

UPGRADE-5.2.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ FrameworkBundle
1616
used to be added by default to the seed, which is not the case anymore. This allows sharing caches between
1717
apps or different environments.
1818

19+
Form
20+
----
21+
22+
* Deprecated `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`.
23+
24+
Before:
25+
26+
```php
27+
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
28+
29+
$builder->setDataMapper(new PropertyPathMapper());
30+
```
31+
32+
After:
33+
34+
```php
35+
use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
36+
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
37+
38+
$builder->setDataMapper(new DataMapper(new PropertyPathAccessor()));
39+
```
40+
1941
Lock
2042
----
2143

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CHANGELOG
55
-----
66

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

911
5.1.0
1012
-----
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 $viewData The view data of the compound form
25+
* @param FormInterface $form The {@link FormInterface()} instance to check
26+
*
27+
* @return mixed The value at the end of the property
28+
*
29+
* @throws Exception\AccessException If unable to read from the given form data
30+
*/
31+
public function getValue($viewData, FormInterface $form);
32+
33+
/**
34+
* Sets the value at the end of the property of the object graph.
35+
*
36+
* @param object|array $viewData The view data of the compound form
37+
* @param mixed $value The value to set at the end of the object graph
38+
* @param FormInterface $form The {@link FormInterface()} instance to check
39+
*
40+
* @throws Exception\AccessException If unable to write the given value
41+
*/
42+
public function setValue(&$viewData, $value, FormInterface $form): void;
43+
44+
/**
45+
* Returns whether a value can be read from an object graph.
46+
*
47+
* Whenever this method returns true, {@link getValue()} is guaranteed not
48+
* to throw an exception when called with the same arguments.
49+
*
50+
* @param object|array $viewData The view data of the compound form
51+
* @param FormInterface $form The {@link FormInterface()} instance to check
52+
*
53+
* @return bool Whether the value can be read
54+
*/
55+
public function isReadable($viewData, FormInterface $form): bool;
56+
57+
/**
58+
* Returns whether a value can be written at a given object graph.
59+
*
60+
* Whenever this method returns true, {@link setValue()} is guaranteed not
61+
* to throw an exception when called with the same arguments.
62+
*
63+
* @param object|array $viewData The view data of the compound form
64+
* @param FormInterface $form The {@link FormInterface()} instance to check
65+
*
66+
* @return bool Whether the value can be set
67+
*/
68+
public function isWritable($viewData, FormInterface $form): bool;
69+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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\Exception;
13+
14+
class AccessException extends RuntimeException
15+
{
16+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Exception\AccessException;
16+
use Symfony\Component\Form\FormInterface;
17+
18+
/**
19+
* Writes and reads values to/from an object or array using callback functions.
20+
*
21+
* @author Yonel Ceruto <yonelceruto@gmail.com>
22+
*/
23+
class CallbackAccessor implements DataAccessorInterface
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function getValue($data, FormInterface $form)
29+
{
30+
if (null === $getter = $form->getConfig()->getOption('getter')) {
31+
throw new AccessException('Unable to read from the given form data as no getter is defined.');
32+
}
33+
34+
return ($getter)($data, $form);
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function setValue(&$data, $value, FormInterface $form): void
41+
{
42+
if (null === $setter = $form->getConfig()->getOption('setter')) {
43+
throw new AccessException('Unable to write the given value as no setter is defined.');
44+
}
45+
46+
($setter)($data, $form->getData(), $form);
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
public function isReadable($data, FormInterface $form): bool
53+
{
54+
return null !== $form->getConfig()->getOption('getter');
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function isWritable($data, FormInterface $form): bool
61+
{
62+
return null !== $form->getConfig()->getOption('setter');
63+
}
64+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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\Exception\AccessException;
16+
use Symfony\Component\Form\FormInterface;
17+
18+
/**
19+
* @author Yonel Ceruto <yonelceruto@gmail.com>
20+
*/
21+
class ChainAccessor implements DataAccessorInterface
22+
{
23+
private $accessors;
24+
25+
/**
26+
* @param DataAccessorInterface[]|iterable $accessors
27+
*/
28+
public function __construct(iterable $accessors)
29+
{
30+
$this->accessors = $accessors;
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function getValue($data, FormInterface $form)
37+
{
38+
foreach ($this->accessors as $accessor) {
39+
if ($accessor->isReadable($data, $form)) {
40+
return $accessor->getValue($data, $form);
41+
}
42+
}
43+
44+
throw new AccessException('Unable to read from the given form data as no accessor in the chain is able to read the data.');
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function setValue(&$data, $value, FormInterface $form): void
51+
{
52+
foreach ($this->accessors as $accessor) {
53+
if ($accessor->isWritable($data, $form)) {
54+
$accessor->setValue($data, $value, $form);
55+
56+
return;
57+
}
58+
}
59+
60+
throw new AccessException('Unable to write the given value as no accessor in the chain is able to set the data.');
61+
}
62+
63+
/**
64+
* {@inheritdoc}
65+
*/
66+
public function isReadable($data, FormInterface $form): bool
67+
{
68+
foreach ($this->accessors as $accessor) {
69+
if ($accessor->isReadable($data, $form)) {
70+
return true;
71+
}
72+
}
73+
74+
return false;
75+
}
76+
77+
/**
78+
* {@inheritdoc}
79+
*/
80+
public function isWritable($data, FormInterface $form): bool
81+
{
82+
foreach ($this->accessors as $accessor) {
83+
if ($accessor->isWritable($data, $form)) {
84+
return true;
85+
}
86+
}
87+
88+
return false;
89+
}
90+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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\Exception\AccessException;
16+
use Symfony\Component\Form\FormInterface;
17+
use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException;
18+
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
19+
use Symfony\Component\PropertyAccess\PropertyAccess;
20+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
21+
22+
/**
23+
* Writes and reads values to/from an object or array using property path.
24+
*
25+
* @author Yonel Ceruto <yonelceruto@gmail.com>
26+
* @author Bernhard Schussek <bschussek@gmail.com>
27+
*/
28+
class PropertyPathAccessor implements DataAccessorInterface
29+
{
30+
private $propertyAccessor;
31+
32+
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
33+
{
34+
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function getValue($data, FormInterface $form)
41+
{
42+
if (null === $propertyPath = $form->getPropertyPath()) {
43+
throw new AccessException('Unable to read from the given form data as no property path is defined.');
44+
}
45+
46+
return $this->getPropertyValue($data, $propertyPath);
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
public function setValue(&$data, $propertyValue, FormInterface $form): void
53+
{
54+
if (null === $propertyPath = $form->getPropertyPath()) {
55+
throw new AccessException('Unable to write the given value as no property path is defined.');
56+
}
57+
58+
// If the field is of type DateTimeInterface and the data is the same skip the update to
59+
// keep the original object hash
60+
if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->getPropertyValue($data, $propertyPath)) {
61+
return;
62+
}
63+
64+
// If the data is identical to the value in $data, we are
65+
// dealing with a reference
66+
if (!\is_object($data) || !$form->getConfig()->getByReference() || $propertyValue !== $this->getPropertyValue($data, $propertyPath)) {
67+
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
68+
}
69+
}
70+
71+
/**
72+
* {@inheritdoc}
73+
*/
74+
public function isReadable($data, FormInterface $form): bool
75+
{
76+
return null !== $form->getPropertyPath();
77+
}
78+
79+
/**
80+
* {@inheritdoc}
81+
*/
82+
public function isWritable($data, FormInterface $form): bool
83+
{
84+
return null !== $form->getPropertyPath();
85+
}
86+
87+
private function getPropertyValue($data, $propertyPath)
88+
{
89+
try {
90+
return $this->propertyAccessor->getValue($data, $propertyPath);
91+
} catch (PropertyAccessException $e) {
92+
if (!$e instanceof UninitializedPropertyException
93+
// For versions without UninitializedPropertyException check the exception message
94+
&& (class_exists(UninitializedPropertyException::class) || false === strpos($e->getMessage(), 'You should initialize it'))
95+
) {
96+
throw $e;
97+
}
98+
99+
return null;
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)