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

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!

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": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}