I'm building a custom form in Drupal 9, which has some ajax callbacks.

During the buildForm I need to load some extra data via a rest call to an external service, which then I put inside a private_tempstore variable.

I'd like to avoid to call the rest end-point during the ajax callbacks, and rely on the stored variable.

However, I can't find a way to differentiate between the "load page" case and the "ajax case". Is it possible?

I've found this answer that seems to generally work:

// Example for brevity only, inject the request_stack service and call 
// getCurrentRequest() on it to get the request object if possible.
$request = \Drupal::request();
$is_ajax = $request->isXmlHttpRequest();

But I'd like to know if there's some helper\solution using the form API.

  • 2
    IMHO no form logic should ever be based on Page load vs. Ajax. Given core's BigPipe might load anything by Ajax (or not) depending on block placement, environment, config, etc..., this seems like a guaranteed road to fiasco.
    – Hudri
    Jul 2 at 8:50
  • @Hudri that's a big consideration I didn't realize, thank you. However, is there another way to solve this question? I mean, I'd like to avoid to do an external rest call to every ajax callback of the form.
    – Giuseppe
    Jul 2 at 8:59
  • TBH I don't quite understand the reason for the question. buildForm has access to FormStateInterface $form_state, what's wrong with $form_state?
    – Hudri
    Jul 2 at 9:07
  • @Hudri I'll try to rephrase: during the first run of the buildForm the data I need is get from the rest endpoint, it is not inside the $form_state. I then store that data inside the tempstore (but I could also use $form_state->setTemporaryValue(). During the following ajax callbacks the data could be thus accessed via $form_state, but I obviously need to differentiate between the first case and the following ones.
    – Giuseppe
    Jul 2 at 9:28
  • 1
    I believe you don't see the wood for all the trees :-) Pseudo-Code function buildForm() { if (!$form_state->get('some_helper_var') { $tempStore = load_external_stuff(); $form_state->set('some_helper_var', TRUE); } $form['field_foo']['default_value'] = $form_state->get('field_foo') ?? $tempStore->get('foo')); }
    – Hudri
    Jul 2 at 9:46

Since Drupal 8 the form object instantiated with buildForm() is not preserved between the request rendering the form and the first Ajax request. So be prepared that buildForm() is called again and has to produce the exact same result. When you get data from $form_state this is not the data you expect from the first buildForm() because this is never cached. Adding to the complexity the rendered result of the first build is cached, so what you store in the first build elsewhere, for example in tempStore, might be outdated in the Ajax request. The only data which works as expected are form values, which can be hidden if you want to have them in the submitted form and not visible to the user.

TLDR: buildForm() is called more often than you think and you shouldn't put code in it which is expensive to run. Refactor the external API call to a service, with proper caching, so that it doesn't matter how often it is called. Invalidate the service cache the same way as the rendered form, so that neither of them has outdated data.

  • 1. thank you for the explanation, although it is not really clear to me. E.g. I don't get how data stored in tempStore can be outdated, if $form_state is not cached, and what relationship the cached rendered result have with that. Is there a more detailed documentation on how this works to get a better idea?
    – Giuseppe
    Jul 5 at 8:56
  • 2. "Invalidate the service cache the same way as the rendered form, so that neither of them has outdated data." How can do that? What should be the "trigger" to invalidate the service cache? I mean, I'd like to do that at page reload, should I use a KernelEvents::REQUEST looking for that specific route? And for the form cache, does that mean that I should simply make the form not cacheable at all?
    – Giuseppe
    Jul 5 at 8:57
  • 1
    1. The main problem is you are trying to use buildform() out of scope. With the same input it has to produce the same result as often as Drupal is calling this method. Only if $form_state contains submitted and processed form values or a triggering element you can rebuild a different $form and store data.
    – 4k4
    Jul 5 at 9:24
  • 1
    2. Add a cache bin to the service and cache with the same cache metadata you have attached to the rendered $form.
    – 4k4
    Jul 5 at 9:24
  • If it's critical that you use the same version of the external API data you could add a version string as hidden form element.
    – 4k4
    Jul 5 at 9:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.