If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeReady 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.
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!
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!
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!
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?
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!
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');
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!
// 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
}
}
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?