Suppose that we have a value object or, more in general, an object where its data has been set in the constructor
1 2 3 4 5 6 7 8 9 10 11 |
class Foo { protected $bar; protected $foobar; public function construct($bar, $foobar) { // .... } } |
and its FormType
1 2 3 4 5 6 7 8 9 10 11 |
class FooType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('bar') ->add('foobar'); } // ... } |
Let’s assume that bar
and foobar
are taken from HTTP POST values (so, basically, we need those values to be posted in order to instantiate a Foo
object).
Question
How can I create the form?
1 2 |
$foo = new Foo(???); $this->createForm(FooType::class, $foo); |
I need to pass an instance of Foo class to the form factory but the Foo class needs, in turn, form values submitted by the user POST action.
This seems like a “circular chain of needs”.
Solution
You can use the empty_data
attribute of FormType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class FooType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('bar') ->add('foobar'); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ ‘empty_data’ => function (FormInterface $interface) { return new Foo($interface->get(‘bar’)->getData(), $interface->get(‘foobar’)->getData()); }, ]); } } |
Explanation
We make form factory accept a null
value during form creation
1 |
$this->createForm(FooType::class, null); |
Then, when we post data, the FormComponent will look for empty_data
option and will instantiate a Foo
object with values passed from post.
“Edit” of Value Object
This will work only if you need to create a new object, if you need to use the same view also for “edit” (quotes because you cannot edit a Value Object; you need to create a brand new one) you have to define a DataMapper and implement two “mapping” functions called mapDataToForms
and mapFormsToData
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 |
class FooType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('bar') ->add('foobar') ->setDataMapper($this); <-- Override default data mapper } public function mapDataToForms($data, $forms) { // This is for "read" data and populate the view $forms = iterator_to_array($forms); $forms['bar']->setData($data ? $data->getBar() : //default value here; $forms['foobar']->setData($data ? $data->getFooBar() : // default value here; } public function mapFormsToData($forms, &data) { // This is for "write" into (create a) object. // Pay attention to $data argument: it's been passed by reference and not copy! $forms = iterator_to_array($forms); $data = new Foo($forms['bar'] } public function configureOptions(OptionsResolver $resolver) { // We don't need empty_data anymore as we're creating object through // data mapper! $resolver->setDefaults(['empty_data’ => null]); } } |
Finally (in both scenarios) if we need to retrieve our object, we can act as follows
1 2 3 4 5 |
$form = $this->createForm(FooType::class, null); $form->handleRequest($request); if ($form->isValid()) { $foo = $form->getData(); } |
If you haven’t read it yet – or if you’ve already done – this article could fit some “fuzziness” of this older article
Beware that the
empty_data
option will not suit every needs. Indeed, it works great for creating an object when initial data are empty, but trying to update data (and thus replace the initial value object by another instance) won’t work.I recommend you to read this great article about value objects in Symfony Forms written by webmozart: https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/.
You can also give a look to this package made in the aim to ease immutable and value objects manipulation with the Symfony Form component –> https://github.com/Elao/FormSimpleObjectMapper. 😉
Hi Maxime.
You’re totally right, I’ve missed the update part but I was aware of that way of handle Value Objects in update phase; I’ll update later.
Thank your for comment and for pointing out your bundle.
Cheers!
S.
While conceptually it’s nice to have value objects like that, I think it’s better to simply use the benefits of the property accessor. As your object will never be in a valid state either way, I think it’s better so use it as a mere data container: https://stovepipe.systems/post/avoiding-entities-in-forms
Hi Iltar, thank you for this comment.
Well, actually, it’s not completely true that “object will never be in a valid state either way” because, through constructor, there are cases where I can be sure that if object is created, it will be valid and ready to be persisted.
“Avoiding entities in forms” seems to be too much restrictive and should be handled from case to case.
Cheers.
S.
be aware that the empty data callback is applied before validation and so $interface->get(‘bar’)->getData() can return a value that may not fit your value object constructor
Hi Thomas, thank you for your comment.
Well, although it’s true that validation is performed *after* data binding, there is no difference with “standard” form handling.
What I mean is that in Symfony the process is post –> bind (so, call setters) –> validation. As you can see, also calls to setters is “afflicted” from this problem. One typical scenario is when you cannot accept a null value and you have
Assert\NotNull()
on the property: if you post anull
value you get an error.To solve this usually you can use a DTO that is less restrictive than your entity (model) and that will perform some controls before pour contents into entity.
What I wanted to highlight with this comment is that’s not a problem of
empty_data
or Immutable Objects or Value Objects. Hope is clear 🙂