[PP-*] Allow both AJAX and non-AJAX forms to POST to dedicated URLs

Created on 9 June 2015, over 9 years ago
Updated 7 March 2024, 10 months ago

This is the successor of #2367555: Deprecate EnforcedResponseException support β†’ , which is the issue that several @todos in Drupal core are referring to.

Follow-up to #2263569: Bypass form caching by default for forms using #ajax. β†’

Problem/Motivation

In #2263569: Bypass form caching by default for forms using #ajax. β†’ forms have been setup to POST ajax requests to their original paths instead of system/ajax.

The form is then 'searched for' on the page during the rendering pipeline and once found an Exception is thrown, the rendering aborted and the ajax callback called.

The reason _why_ a form needs to be searched is that the form is displayed in context of a block, a controller or whatever.

e.g

// deep down somewhere in the code

$some_form = new SomeForm();

$build['my_form'] = $this->formBuilder->getForm($some_form);

As long as $some_form defines FormInterface and has appropriate getFormId(), buildForm(), validateForm() and submitForm() options that works.

However what now happens is that someone does:

// deep down somewhere in the code

$some_form = new SomeForm($some_entity, $some_service_to_use);

$build['my_form'] = $this->formBuilder->getForm($some_form);

that means that SomeForm can hold _state_, which is similar to the default parameters provided in Drupal 7. (drupal_get_form('form_id', $some_value, $some_entity, ...))

But what is worse is that SomeForm also might depend on FormState containing some of that external state and then it gets difficult.

Also consider this form living in a block plugin somewhere in the code base.

Proposed resolution

"Interfaces are the answer to the life, the universe and everything." (or was that 42?)

Add a new IndependentFormInterface / ReconstructableFormInterface / LazyBuilderInterface ...

and provide the means for the form to re-create itself.

e.g.

class ReconstructibleSomeForm extends SomeForm implements LazyBuilderInterface {

  public static function lazyBuild(ContainerInterface $container, $entity_type, $entity_id) {
     $entity = Entity::load($entity_type, $entity_id);
     return new static(
        $entity,
        $container->get('some_service'),
     );
  }

  public function getLazyBuilderArguments() {
     return [ $this->entity->entityType(), $this->entity->id() ]; // Must only be scalar values or null.
  }
}

When the form builder encounters a form implementing that interface AND this is a GET request, then it calls the getReconstructContext() method, ensures it only contains scalar values (very similar to #lazy_builder's) and creates the following render array:


$key = NULL;

if ($form instanceof LazyBuilderInterface) {
  $args = $form->getLazyBuilderArguments();

  // Put real callback to the start of the arguments.
  array_unshift($args, get_class($form) . '::lazyBuild');

  $fast_path_renderable = [
    '#lazy_builder' => [ 'lazy_form_builder:buildForm', $args ],
  ];

  $key = 'lazy_form_builder_' . hash('sha1', serialize($fast_path_renderable));

  // Same as we fixed the autocomplete issue ...
  if (!$this->keyValue->get($key)) {
    $this->keyValue->set($key, $fast_path_renderable);
  }
}

if ($key) {
  // Set the _form_ajax GET argument to $key.
}

Then in onController (pseudo-code):

if (isset($_GET['_form_ajax']) && $_GET['_form_ajax'] != 1) {
  $fast_path_renderable = $this->keyValue->get($_GET['_form_ajax']);
  $controller = 'lazy_builder_controller:execute';
  $arguments = $fast_path_renderable;
}

Then in LazyBuilderController:


class LazyBuilderController extends Controller {

  public function __construct(Renderer $renderer) {
    $this->renderer = $renderer;
  }

  public function execute($fast_path_renderable) {
     return $this->renderer->render($fast_path_renderable);
  }
}

class LazyFormBuilder {

  public function buildForm() {
     $args = func_get_args();
     $executable = array_shift($args);
     $form = call_user_func_array($executable, $args);
     return $this->formBuilder->getForm($form);
  }
}

Note: This is idealized from what I plan for ESI (as its the exact same mechanism), so for this issue _could_ e.g. combine LazyFormBuilder and LazyBuilderController.

The idea however remains the same.

Remaining tasks

- Discuss
- Do it

User interface changes

- None

API changes

- New Interface

πŸ“Œ Task
Status

Postponed

Version

11.0 πŸ”₯

Component
FormΒ  β†’

Last updated 2 days ago

Created by

πŸ‡©πŸ‡ͺGermany Fabianx

Live updates comments and jobs are added and updated live.
  • Performance

    It affects performance. It is often combined with the Needs profiling tag.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡¬πŸ‡§United Kingdom catch

    Postponing this on πŸ“Œ Make POST requests render cacheable Needs work which is much simpler (if cleverer) and will achieve similar results without having to come up with a new access layer.

  • Status changed to Active 10 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    πŸ“Œ Make POST requests render cacheable Needs work is in.

    What's left here? #30 makes it sound like there's nothing left to do?

  • Status changed to Postponed 10 months ago
  • πŸ‡¬πŸ‡§United Kingdom catch

    πŸ“Œ Make POST requests render cacheable Needs work fixes most of the performance issue here (there's follow-up/parallel work left to do like making forms cacheable and putting them in lazy builders etc.).

    However this issue is still valid in the sense that EnforcedResponseException is weird and we're still rendering entire pages to find forms in deeply nested arrays even if we're now doing so with some caching of some of those arrays enabled. If we submitted especially AJAX forms to a dedicated form action URL, we'd know the form ID in advance, and wouldn't need to 'detect' the form at all. However doing this for non-AJAX forms (or AJAX forms with JavaScript disabled) is going to be a lot trickier because we need to render the whole page to show validation errors, not to mention access issues.

    #2504115: AJAX forms should submit to $form['#action'] instead of β†’ is probably the first step towards this, so marking postponed on that instead.

Production build 0.71.5 2024