Symfony form is a standalone component you can use to save a lot of time when developing an application that needs forms to interact with users (checkout my talk and slides from SfDayIt 2016 in Rome)
As every powerful component/library/extension, power comes to a price. The price to pay for using Symfony form component is that everything outside its “ordinary” usage (ie.: bind an entity and perform validation) is perceived as difficult as remember all decimal digits of PI.
As a matter of fact not everything outside the ordinary usage is easy but it’s certainly true that not all these tasks are hard to tackle if you’re not a form super hero: Data transformers are one of those concept not so hard to understand and implement in your own application.
Data transformers
Data transformers are useful when model data format (eg.: entity data, db data, …) is different from form/view one.
Let’s imagine you need to display a date field with three different values (day, month, year) but model is a single string (luckily for us, Symfony DateType comes with this kind of transformation): data transformers comes in handy as you can manipulate data and perform this kind of transformation.
There are three data type (or data “stage” if you prefer) when working with forms
- Model data – Data of the model (entity, db, file system, …)
- Norm data – Normalized data; most of the time is the same of model data and it’s not commonly used directly
- View data – Data of the form
https://symfony.com/doc/current/form/data_transformers.html#about-model-and-view-transformers
So model data is “YYYY-mm-dd” string that is transformed in three single string fields “YYYY”, “mm”, “dd”.
Transformers can help you a lot to decouple view from model.
A real example: single entity field to multiple form fields
To understand better these concepts, let’s think about this implementation: we need to store a duration in minutes of an event happening every week in a particular day, bounded to a particular user. One solution could be to store for each weekday a record with this value. As we can’t see a single valid motivation behind this choice, the most suitable solution is to store a single array field per user. Here comes the troubles: we need a single value (json) but we don’t want to make UX an absolute nightmare, so we decided to let the user input those values in hour:minutes (two distinct form fields) for each single day.
So the issue is: display seven distinct fields, with two possible values each (hour and minutes), but store a string containing an array of minutes for each day.
Transform minutes into hour / minutes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class HourMinuteType extends AbstractType { const HOUR_KEY = 'hour'; const MINUTE_KEY = 'minute'; /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add(self::HOUR_KEY, NumberType::class, [ 'constraints' => [ new Range(['min' => 0]), ], ]) ->add(self::MINUTE_KEY, NumberType::class, [ 'constraints' => [ new Range(['min' => 0, 'max' => 59]), ], ]) ->addModelTransformer(new CallbackTransformer( function ($minutes) { return [ self::HOUR_KEY => $minutes ? (int) ($minutes / 60) : null, self::MINUTE_KEY => $minutes ? $minutes % 60 : null, ]; }, function (array $hourMinute) { $hours = $hourMinute[self::HOUR_KEY] * 60; return $hours + $hourMinute[self::MINUTE_KEY]; } )); } } |
This form type could be used for each week day to transform from $minutes
(model
data) to hours and minutes (view
data). First callback handles model
— to –> view
data format. As db data is a single scalar value and form data is composed by two values, we need to return an array with this conversion. Second callback handles the opposite transformation: it takes the array and converts to single scalar value. Please note that this form type has no data_class
and its data will be an array indexed with constant keys that could be used in week days form. Furthermore note that array keys are the same of form field keys.
Transform week days
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class WeekEventDurantionType extends AbstractType { const SUNDAY = 'sunday'; const MONDAY = 'monday'; const TUESDAY = 'tuesday'; const WEDNESDAY = 'wednesday'; const THURSDAY = 'thursday'; const FRIDAY = 'friday'; const SATURDAY = 'saturday'; /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add(self::SUNDAY, HourMinuteType::class) ->add(self::MONDAY, HourMinuteType::class) ->add(self::TUESDAY, HourMinuteType::class) ->add(self::WEDNESDAY, HourMinuteType::class) ->add(self::THURSDAY, HourMinuteType::class) ->add(self::FRIDAY, HourMinuteType::class) ->add(self::SATURDAY, HourMinuteType::class) ->addModelTransformer(new CallbackTransformer( function ($dbDays) { return [ self::SUNDAY => $dbDays[0] ?? null, self::MONDAY => $dbDays[1] ?? null, self::TUESDAY => $dbDays[2] ?? null, self::WEDNESDAY => $dbDays[3] ?? null, self::THURSDAY => $dbDays[4] ?? null, self::FRIDAY => $dbDays[5] ?? null, self::SATURDAY => $dbDays[6] ?? null, ]; }, function ($formDays) { return [ 0 => $formDays[self::SUNDAY], 1 => $formDays[self::MONDAY], 2 => $formDays[self::TUESDAY], 3 => $formDays[self::WEDNESDAY], 4 => $formDays[self::THURSDAY], 5 => $formDays[self::FRIDAY], 6 => $formDays[self::SATURDAY], ]; } )); } } |
As the previous form type, we take advantage of data transformer to manipulate data. Notice that first callback that handles db data (indexed with 0,1,2,…6 where 0 is Sunday and 6 is Monday according to PHP date “w” format) will return an array indexed with same fields as form fields (each for a week day) and reverse transform will do the opposite: take fields and transform them into an array with indexes as numbers (as explained previously). Moreover each field has the HourMinuteType
shown above.
Results
You can use WeekEventDurantionType
to map an array
or simple_array
doctrine field and everything will work straightforwardly.
This is our form (in Italian language where Lunedì is Monday and Domenica is Sunday; Ore is Hours and Minuti is Minutes).
This is the database content
[0, 130, 0, 60, 0, 0, 20]
Data transformers are a powerful concept behind Symfony forms and know them can reduce dramatically the amount of code needed to handle situation where data can change from storage to view and vice versa.
Little typo in class HourMinuteType, rows 30 and 32:
ORE_KEY and MINUTI_KEY instead of HOUR_KEY and MINUTE_KEY.
Thanks Michele.
Fixed.
This is really helpful for me