Skip to content

Commit b1cb1c0

Browse files
vudaltsovxabbuh
authored andcommitted
[Forms] Added data-mapper docs
1 parent d2dce7f commit b1cb1c0

File tree

3 files changed

+204
-2
lines changed

3 files changed

+204
-2
lines changed

form/data_mappers.rst

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
.. index::
2+
single: Form; Data mappers
3+
4+
When and How to Use Data Mappers
5+
================================
6+
7+
When a form is compound, the initial data needs to be passed to children so each can display their
8+
own input value. On submission, children values need to be written back into the form.
9+
10+
Data mappers are responsible for reading and writing data from and into parent forms.
11+
12+
The main built-in data mapper uses the :doc:`PropertyAccess component </components/property_access>`
13+
and will fit most cases. However, you can create your own implementation that
14+
could, for example, pass submitted data to immutable objects via their constructor.
15+
16+
The Difference between Data Transformers and Mappers
17+
----------------------------------------------------
18+
19+
It is important to know the difference between
20+
:doc:`data transformers </form/data_transformers>` and mappers.
21+
22+
* **Data transformers** change the representation of a value (e.g. from
23+
``"2016-08-12"`` to a ``DateTime`` instance);
24+
* **Data mappers** map data (e.g. an object or array) to form fields, and vice versa.
25+
26+
Changing a ``YYYY-mm-dd`` string value to a ``DateTime`` instance is done by a
27+
data transformer. Populating inner fields (e.g year, hour, etc) of a compound date type using
28+
a ``DateTime`` instance is done by the data mapper.
29+
30+
Creating a Data Mapper
31+
----------------------
32+
33+
Suppose that you want to save a set of colors to the database. For this, you're
34+
using an immutable color object::
35+
36+
// src/App/Painting/Color.php
37+
namespace App\Painting;
38+
39+
final class Color
40+
{
41+
private $red;
42+
private $green;
43+
private $blue;
44+
45+
public function __construct(int $red, int $green, int $blue)
46+
{
47+
$this->red = $red;
48+
$this->green = $green;
49+
$this->blue = $blue;
50+
}
51+
52+
public function getRed(): int
53+
{
54+
return $this->red;
55+
}
56+
57+
public function getGreen(): int
58+
{
59+
return $this->green;
60+
}
61+
62+
public function getBlue(): int
63+
{
64+
return $this->blue;
65+
}
66+
}
67+
68+
The form type should be allowed to edit a color. But because you've decided to
69+
make the ``Color`` object immutable, a new color object has to be created each time
70+
one of the values is changed.
71+
72+
.. tip::
73+
74+
If you're using a mutable object with constructor arguments, instead of
75+
using a data mapper, you should configure the ``empty_data`` option with a closure
76+
as described in
77+
:ref:`How to Configure empty Data for a Form Class <forms-empty-data-closure>`.
78+
79+
The red, green and blue form fields have to be mapped to the constructor
80+
arguments and the ``Color`` instance has to be mapped to red, green and blue
81+
form fields. Recognize a familiar pattern? It's time for a data mapper!
82+
83+
.. code-block:: php
84+
85+
// src/App/Form/DataMapper/ColorMapper.php
86+
namespace App\Form\DataMapper;
87+
88+
use App\Painting\Color;
89+
use Symfony\Component\Form\DataMapperInterface;
90+
use Symfony\Component\Form\Exception\UnexpectedTypeException;
91+
use Symfony\Component\Form\FormInterface;
92+
93+
final class ColorMapper implements DataMapperInterface
94+
{
95+
/**
96+
* @param Color|null $data
97+
*/
98+
public function mapDataToForms($data, $forms)
99+
{
100+
// there is no data yet, so nothing to prepopulate
101+
if (null === $data) {
102+
return;
103+
}
104+
105+
// invalid data type
106+
if (!$data instanceof Color) {
107+
throw new UnexpectedTypeException($data, Color::class);
108+
}
109+
110+
/** @var FormInterface[] $forms */
111+
$forms = iterator_to_array($forms);
112+
113+
// initialize form field values
114+
$forms['red']->setData($data->getRed());
115+
$forms['green']->setData($data->getGreen());
116+
$forms['blue']->setData($data->getBlue());
117+
}
118+
119+
public function mapFormsToData($forms, &$data)
120+
{
121+
/** @var FormInterface[] $forms */
122+
$forms = iterator_to_array($forms);
123+
124+
// as data is passed by reference, overriding it will change it in
125+
// the form object as well
126+
// beware of type inconsistency, see caution below
127+
$data = new Color(
128+
$forms['red']->getData(),
129+
$forms['green']->getData(),
130+
$forms['blue']->getData()
131+
);
132+
}
133+
}
134+
135+
.. caution::
136+
137+
The data passed to the mapper is *not yet validated*. This means that your
138+
objects should allow being created in an invalid state in order to produce
139+
user-friendly errors in the form.
140+
141+
Using the Mapper
142+
----------------
143+
144+
You're ready to use the data mapper for the ``ColorType`` form. Use the
145+
:method:`Symfony\\Component\\Form\\FormConfigBuilderInterface::setDataMapper`
146+
method to configure the data mapper::
147+
148+
// src/App/Form/Type/ColorType.php
149+
namespace App\Form\Type;
150+
151+
use App\Form\DataMapper\ColorMapper;
152+
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
153+
154+
final class ColorType extends AbstractType
155+
{
156+
public function buildForm(FormBuilderInterface $builder, array $options)
157+
{
158+
$builder
159+
->add('red', IntegerType::class, array(
160+
// enforce the strictness of the type to ensure the constructor
161+
// of the Color class doesn't break
162+
'empty_data' => '0',
163+
))
164+
->add('green', IntegerType::class, array(
165+
'empty_data' => '0',
166+
))
167+
->add('blue', IntegerType::class, array(
168+
'empty_data' => '0',
169+
))
170+
->setDataMapper(new ColorMapper())
171+
;
172+
}
173+
174+
public function configureOptions(OptionsResolver $resolver)
175+
{
176+
// when creating a new color, the initial data should be null
177+
$resolver->setDefault('empty_data', null);
178+
}
179+
}
180+
181+
Cool! When using the ``ColorType`` form, the custom ``ColorMapper`` will create
182+
a new ``Color`` object now.
183+
184+
.. caution::
185+
186+
When a form has the ``inherit_data`` option set to ``true``, it does not use the data mapper and
187+
lets its parent map inner values.
188+
189+
.. tip::
190+
191+
You can also implement the ``DataMapperInterface`` in the ``ColorType`` and add
192+
the ``mapDataToForms()`` and ``mapFormsToData()`` in the form type directly
193+
to avoid creating a new class. You'll then have to call
194+
``$builder->setDataMapper($this)``.

form/data_transformers.rst

+8-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ to render the form, and then back into a ``DateTime`` object on submit.
1313

1414
.. caution::
1515

16-
When a form field has the ``inherit_data`` option set, Data Transformers
17-
won't be applied to that field.
16+
When a form field has the ``inherit_data`` option set to ``true``, data transformers
17+
are not applied to that field.
18+
19+
.. seealso::
20+
21+
If, instead of transforming the representation of a value, you need to map
22+
values to a form field and back, you should use a data mapper. Check out
23+
:doc:`/form/data_mappers`.
1824

1925
.. _simple-example-sanitizing-html-on-user-input:
2026

form/use_empty_data.rst

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ The point is, you can set ``empty_data`` to the exact "new" object that you want
7777
In order to pass arguments to the ``BlogType`` constructor, you'll need to
7878
:doc:`register it as a service and tag with form.type </form/form_dependencies>`.
7979

80+
.. _forms-empty-data-closure:
81+
8082
Option 2: Provide a Closure
8183
---------------------------
8284

0 commit comments

Comments
 (0)