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