More praise for decorators

Our problem was the marketing team wanted information in Marketo about our visitor struggling with forms. Makes total sense. Better explanation of what was expected, more client side validation etc makes for a smoother experience. However, hook_ajax_render_alter only contains the AJAX commands being sent and does not have any form information and the myriad extendsion points in Form API do not have access to the AJAX commands. What now?

A little background

One of the most important feature of Drupal has always been extensibility. It had the hook system since the dawn of time which allowed adding and changing data structures at various points of the code flow. However, rare cases have been always been a problem: what if a hook was not available? It’s fairly impossible to think of every possible use case ahead of the time, after all.

Another extension point was the ability to replace certain include files wholesale, for example to facilitate different path alias storages.

In Drupal 8 both still exist but vastly expanded. Events joined hooks, and a ton of functionality is in plugins which are identified by their id and the class providing the relevant functionality — similarly to the include files — can be replaced wholesale.

Now, all this replaceability is great but what happens when two modules want to replace the same file? Their functionality might not even collide, they might want to change different methods but as the replaceability is class level, there is no other choice but to replace the entire class. Note the situation is not always this bad because of derivatives — it’s possible originally one class provided the functionality for say every entity type but if only a specific entity type needs a different implementation, it’s possible to provide a plugin class for a derivative, see the NodeRow class in Views for a simple example.

Now, for plugins we have no other choices but the complete replacement with the derivative functionality as described providing some relief but a lot of functionality is in services. And while there is alter functionality for services which is neither a hook nor an event because both depend on services it suffers from the same problem: what happens when two modules want to replace the same service?

Thankfully, for services there is a better way, they are called decorators.

Decorators

For the original problem we needed to find the bridge between form API and AJAX commands — there must be one!

Indeed, the form_ajax_response_builder service implements an interface with just one method which receives the form API information and an initial set of commands and builds a response out of it. It’s real lucky this was architected like this — the only non-test call in core calls it with an empty set of initial commands and so it wouldn’t have been unreasonable to not have this argument and then we would be in a pickle but as it is, we can decorate it. This means our service will replace the original but at the same time the original will not be tossed but rather renamed and passed to ours and we will call it:

sd8.form_ajax_response_builder:
  decorates: form_ajax_response_builder
  class: Drupal\sd8\Sd8FormAjaxResponseBuilder
  arguments: ['@sd8.form_ajax_response_builder.inner', '@marketo_ma']
  

And the shape of the class is this:

class Sd8FormAjaxResponseBuilder implements FormAjaxResponseBuilderInterface {
  
  public function __construct(FormAjaxResponseBuilderInterface $ajaxResponseBuilder, MarketoMaServiceInterface $marketoMaService) {
    $this->ajaxResponseBuilder = $ajaxResponseBuilder;
    $this->marketoMaService = $marketoMaService;
  }    

  public function buildResponse(Request $request, array $form, FormStateInterface $formState, array $commands) {
    // Custom code comes here adds commands to $commands to taste.
    // ...
    // And then we call the original.
    return $this->ajaxResponseBuilder->buildResponse($request, $form, $formState, $commands);
  }
}

The name of our service is 100% irrelevant as it’ll be renamed to form_ajax_response_builder. Now if two modules want to mess with AJAX forms, they do not step on each others toes. We do not rely at all on the form_ajax_response_builder service being the core implementation. Although with just a single method it is less important but take care of implementing every method of the interface and call the inner service instead of extending the core original and just overriding the one you need. You can’t know whether the service you decorate will always be the core functionality. Be a good neighbour. It’s only a bit more work, mostly simple typing. And as the Writing the tranc module article mentioned, you might discover some bugs and problems when properly delegating.

So this is alter on steroids: if we need to change some functionality provided by a service and no official method exists, you can decorate it, write some boilerplate implementing the interface by calling the inner service methods and Bob’s your uncle.


Date
September 28, 2020