|
| 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)``. |
0 commit comments