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
Comments
Comment #1
fabianx commentedComment #2
tim.plunkettGuessing this was cloned from the other issue, it doesn't need to be assigned to @effulgentsia just yet.
Comment #3
catchMarking #2367555: Deprecate EnforcedResponseException support as duplicate. That's the older issue, but this is more relevant to current status.
Comment #4
catchThis was discussed in the performance meeting at DrupalCon Barcelona. Doing this would be one of the most significant visitor-facing performance changes we could make - for example the comment form on this page, node forms doing a quicker POST could save potentially seconds with quite small changes.
Comment #5
wim leersComment #6
almaudoh commentedAny hope of this in 8.1.x ??
Comment #7
dawehner@almaudoh
Well, the best thing is for your to try to work on it.
Comment #9
wim leersThis also causes pain for streamed responses: #2702609: Disable placeholdering (and BigPipe) on unsafe requests to help find forms as fast as possible (to allow EnforcedResponses to work).
Comment #10
wim leersComment #11
wim leersComment #14
wim leersSee #2851984-11: Add "create" link template to User entity type annotation, to allow POSTing to /user instead of /entity/user and onwards: this not being a reality yet is once again screwing us.
Comment #15
dawehnerCould we introduce a /form route which allows to take an actual path, so like /form?path=user, which we use inside the form rendering process?
This would then do a subrequest by default. When we do that we can be more sure that we can actually serve form from different URLs. Once this is sure, people can create specific routes and use them somehow in the form.
Comment #18
andypostComment #26
catchStill very valid, but moving this to a task since it's an API improvement as opposed to a functional bug.
Comment #30
catchPostponing this on #3395776: Make POST requests render cacheable which is much simpler (if cleverer) and will achieve similar results without having to come up with a new access layer.
Comment #31
wim leers#3395776: Make POST requests render cacheable is in.
What's left here? #30 makes it sound like there's nothing left to do?
Comment #32
catch#3395776: Make POST requests render cacheable 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
EnforcedResponseExceptionis 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 <current> is probably the first step towards this, so marking postponed on that instead.