Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Events: A readonly Embedded Field

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ready for the last challenge? All four of these genus scientists are already saved to the database. And while I guess it's kind of cool that I can change this scientist from one User to another, it's also a little bit weird: When would I ever change a specific scientist from one User to another? If this User weren't studying this Genus anymore, I should delete them. And if a new User were studying this Genus, we should probably just add a new GenusScientist.

So I want to update the interface: when I hit "Add Another Scientist", I do want the User select, just like now. But for existing genus scientists - the ones that are already saved to the database - I want to simply print the user's email in place of the drop-down.

In Symfony language, this means that I want to remove the user field from the embedded form if the GenusScientist behind it is already saved.

About Form Events

To do that, open the GenusScientistEmbeddedForm. Guess what? We get to try a feature that I don't get to use very often: Symfony Form Events.

Here's the idea: every form has a life cycle: the form is created, initial data is set onto the form and then the form is submitted. And we can hook into this process!

Form Event Setup!

To do it, write addEventListener() and then pass a constant FormEvents::POST_SET_DATA. After that, say array($this, 'onPostSetData'):

... lines 1 - 11
use Symfony\Component\Form\FormEvents;
... lines 13 - 14
class GenusScientistEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 20 - 27
->addEventListener(
FormEvents::POST_SET_DATA,
array($this, 'onPostSetData')
)
;
}
... lines 34 - 50
}

Let's break that down: the POST_SET_DATA is a constant for an event called form.post_set_data. This is called after the data behind the form is added to it: in other words, after the GenusScientist is bound to each embedded form.

When that happens, the form system will call an onPostSetData() function, which we are about to create: public function onPostSetData(). This will receive a FormEvent object:

... lines 1 - 10
use Symfony\Component\Form\FormEvent;
... lines 12 - 14
class GenusScientistEmbeddedForm extends AbstractType
{
... lines 17 - 34
public function onPostSetData(FormEvent $event)
{
... lines 37 - 40
}
... lines 42 - 50
}

Now we're close! Inside, add an if statement: if $event->getData():This form is always bound to a GenusScientist object. So this will return the GenusScientist object bound to this form, or - if this is a new form - then it may return null. That's why we'll say if $event->getData() && $event->getData()->getId():

... lines 1 - 14
class GenusScientistEmbeddedForm extends AbstractType
{
... lines 17 - 34
public function onPostSetData(FormEvent $event)
{
if ($event->getData() && $event->getData()->getId()) {
... lines 38 - 39
}
}
... lines 42 - 50
}

In human-speak: as long as there is a GenusScientist bound to this form and it's been saved to the database - i.e. it has an id value - then let's unset the user field from the form.

To do that, fetch the form with $form = $event->getForm(). Then, literally, unset($form['user']):

... lines 1 - 14
class GenusScientistEmbeddedForm extends AbstractType
{
... lines 17 - 34
public function onPostSetData(FormEvent $event)
{
if ($event->getData() && $event->getData()->getId()) {
$form = $event->getForm();
unset($form['user']);
}
}
... lines 42 - 50
}

This $form variable is a Form object, but you can treat it like an array, including unsetting fields.

That's it for the form! The last step is to conditionally render the user field. Because if we refresh right now, the form system yells at us:

There's no user field inside of our template at line 9.

Wrap that in an if statement: if genusScientistForm.user is defined, then print it:

... lines 1 - 2
{% macro printGenusScientistRow(genusScientistForm) %}
<div class="col-xs-4 js-genus-scientist-item">
... lines 5 - 8
{% if genusScientistForm.user is defined %}
{{ form_row(genusScientistForm.user) }}
... lines 11 - 12
{% endif %}
... line 14
</div>
{% endmacro %}
... lines 17 - 58

Else, use a strong tag and print the user's e-mail address with genusScientistForm.vars - which is something we mastered in our Form Theming tutorial - .data - which will be a GenusScientist object - .user.email:

... lines 1 - 2
{% macro printGenusScientistRow(genusScientistForm) %}
<div class="col-xs-4 js-genus-scientist-item">
... lines 5 - 8
{% if genusScientistForm.user is defined %}
{{ form_row(genusScientistForm.user) }}
{% else %}
<strong>{{ genusScientistForm.vars.data.user.email }}</strong>
{% endif %}
... line 14
</div>
{% endmacro %}
... lines 17 - 58

This says: find the GenusScientist object behind this form, call getUser() on it, and then call getEmail() on that.

I think it's time to celebrate! Refresh the form. It looks exactly like I wanted. It's like my birthday! And when we add a new one, it still has the drop-down. You guys are the best!

Leave a comment!

6
Login or Register to join the conversation
Default user avatar
Default user avatar julien moulis | posted 5 years ago

Hi. I have a question. I tried to apply this readonly tips on my own app, but I'm facing a pb.
Example:
- I have 2 scientists for genus1, I add a new scientist. Then when I hit save, it deletes the 2 scientists and then add the new one.
- If I hit save without doing any changes, it deletes the actual genus scientists.
This is done as soon as I set up this readonly change
Any Idea?

Reply

Yo julien moulis!

Hmm. So, we're currently investigating a possible issue with the collection - there have been 2 other reports of weird issues. This stuff is way trickier than it needs to be :).

If you download the finish code from this course, do you see the same behavior? I'm curious if you're also hitting a weird issue, or if it's some small difference between your app and mine. Either way, sorry about the issues!

Cheers!

Reply
Default user avatar
Default user avatar julien moulis | weaverryan | posted 5 years ago

Hey I got it. So I have the same entities structure than the genus/scientis/genusScientist - Interview/user/interviewUser
In the InterviewUser join entity I set a extra field (boolean) that i didn't rendered on the form ( I did not need it here) and didn't set to any values in the constructor, so it was a null value. I guess doctrine 'thought' that because it was null, it had to be deleted. As soon as I sat a default value in the constructor, everything is working. But I still wander if it is best to use api ajax call than working with this prototype stuff... I would like to reproduce the add attendee google calendar... And I think it would be easier with the Api Ajax way. What do you think?

Reply

Hey julien moulis!

Fascinating! When I play with this stuff again, I'll try playing around with a similar setup. I cannot thing of why this would cause any difference - but, again, this stuff is hard!

And honestly, yes, in most cases, I would use a richer, AJAX-based interface rather than the complex collections situation. It's not only more reliable (because, as we know, this collection stuff is tough!) it's also a better use experience. It might take a bit more time to setup... which is why the collection stuff is nice in *some* situations (e.g. an admin interface... especially if your setup isn't too complex).

Cheers!

Reply

Hi . some help plz, i'm trying to enhance the application above , here we can only add one yearsStudied filed for every user .
Any idea How to: have many yearsStudied fileds for exmple the user1 have stadied 1 year php,2 year c++ ,2 year java and 1 year assembly.. i tried to change
->add('yearsStudied')
to :
->add('yearsStudied', CollectionType::class, array(
'entry_type' => YearsStudiedType::class,
),
));

In YearsStudiedType.php :
$builder->add('year_value');

Reply

Hey Ahmed!

That's an interesting feature you want to implement :)

You would have to do something similar to what we have done here with our "User - GenusScientist" relationship, where one GenusScientist might have many YearsStudied (OneToMany), you will have to create that new YearsStudied Entity, setup the relationship and create a new Admin Area for management .

I think you can re-apply all you have learnt in this tutorial in order to achieve your goal ;)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice