Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

PRE_SET_DATA: Data-based Dynamic Fields

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

On our form class, we're creating the specificLocationName field in two places: it's up in buildForm() and duplicated down inside of setupSpecificLocationNameField(). Because duplication is a bummer, let's fix it by calling $this->setupSpecificLocationNameField() from buildForm().

Except... hmm, there's a minor mismatch: in buildForm(), we're working with a form builder object, but the method expects a FormInterface object. It's a weird situation where these two objects happen to have the same add() method, but they are two totally different classes.

We're going to work around this by leveraging another form event. Remove the block where we first add the specificLocationName field. Oh, and we can remove the $location variable now too.

Let's think about how we could re-add this field using events: we basically want Symfony to call our callback, the moment the underlying "data" is set onto the form - the Article object. Use $builder->addEventListener() and listen on an event called FormEvents::PRE_SET_DATA. Two things: first, this time, we're attaching the event to the entire form, which means our callback will be passed info about the entire form. That's usually what you want: listening to a single field like we did before was a bit of a hack to allow us to remove and re-add the field at just the right moment.

... lines 1 - 27
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 30 - 60
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
... lines 63 - 74
);
... lines 76 - 86
}
... lines 88 - 152

Second, how do we know to use PRE_SET_DATA? When exactly is that called? Open ArticleAdminController: in the edit() action, we pass createForm() an Article object. When that happens, Symfony dispatches this PRE_SET_DATA event. In general, the FormEvents class itself is a great resource for finding out when each event is called and what you can do by listening to it. I won't do it here, but if you hold Command or Ctrl and click the event name to open that class, you'll find great documentation above each constant.

Add the callback with the same FormEvent $event argument. Then, get the underlying data with $data = $event->getData(). We know that this must be either an Article object or possibly null. If there is no data, just return and do nothing: we don't want to add the field at all for the new form.

... lines 1 - 60
$builder->addEventListener(
... line 62
function (FormEvent $event) {
/** @var Article|null $data */
$data = $event->getData();
if (!$data) {
return;
}
... lines 69 - 73
}
);
... lines 76 - 152

If there is data, call $this->setupSpecificLocationNameField() and pass it $event->getForm(). This time, $event->getForm() will be the top-level form, because we added the listener to the top-level builder. For the location, pass $data->getLocation().

... lines 1 - 60
$builder->addEventListener(
... line 62
function (FormEvent $event) {
... lines 64 - 69
$this->setupSpecificLocationNameField(
$event->getForm(),
$data->getLocation()
);
}
);
... lines 76 - 152

Cool! This code should work just like before. But actually, while events are nice, if I need to tweak my form based on the underlying data - like we're doing here - I prefer to avoid using events and just use the $options['data'] key. It's just a bit simpler. But, both solutions are fine.

Anyways, let's try it! I'll hit enter on the address bar to get a fresh page. And... yep! Because "Near a star" is selected as the location, the next field loaded with the correct list of stars.

We are now fully ready for the last, fancy step: adding JavaScript and AJAX to dynamically change the specificLocationName select options when the location changes. And... that's probably the easiest part!

Leave a comment!

3
Login or Register to join the conversation
Default user avatar

When one form field is dependent on value of other field and that depends on another field, "middle" dependent and even depending field, loaded dynamically with PRE_SET_DATA event, isn't initialized in buildForm method and so mentioned POST_SUBMIT listener
adding to this field fails. How should be this solved?

Reply

Hey Ben!

Great timing! We're having a conversation about this RIGHT now - over on another comment thread: https://symfonycasts.com/sc...

Let me know if that solution works... and if not - you can join that conversation :).

Cheers!

Reply
Default user avatar

Hi, thanks for notification, I have checked full thread and replied there... too much.

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6
    }
}
userVoice