Quantcast
Viewing all articles
Browse latest Browse all 295180

OOP hooks using attributes and event dispatcher

Problem/Motivation

For a very long time we wanted to introduce hooks in some OOP manner. The following goals altogether however have proven elusive

  1. No magic naming in the implementations.
  2. A hook implementation should be simple and have minimal boilerplate.
  3. Calling any hook without defining anything should be possible as it is now.
  4. Reordering hooks should be doable.
  5. Relative reordering should be easier. ckeditor5_module_implements_alter is ouch.
  6. Minimal added code to core.

Proposed resolution

Big kudos to EclipseGc for realizing the Symfony EventDispatcherInterface has a getListeners method which allows us to call listeners any way we want. Unlike dispatch this allows us to call listeners with any arguments we want without defining an event object. dpi pioneered attributes for hooks on Hux. I also advocate for attributes but simpler. In short:

namespace Drupal\node\Hook;
class NodeHooks {
  #[Hook('user_cancel')]
  public function userCancel($edit, UserInterface $account, $method) {

In detail:

  1. Hook implementations go into Drupal\modulename\Hook namespace (subdirectories work too). Familiar pattern from plugins. These are automatically registered as autowired services with the class name as the service id. This makes for minimal boilerplate. If autowire doesn't suffice -- should be very rare -- they can be registered manually as well, the service id is the class name.
  2. Hook implementations needs to be marked with a Hook attribute. This is new. Either on a method #[Hook('user_cancel')] or on the class #[Hook('user_cancel', method: 'UserCancel')]. If the class Hook doesn't specify a method, __invoke is the default method.
  3. The attribute supports a priority as well: #[Hook('user_cancel', priority: -20)]. The priority defaults to such values as to keep module order.
  4. The edge case of "implementing a hook on behalf of another module" is also supported by simply specifying the module in the attribute. It defaults to the defining module.
  5. This attribute is patterned on the Symfony AsEventListener attribute which is shipped with the event dispatcher but it is only used in the full Symfony framework.
  6. Multiple implementations are totally allowed on multiple axis: one method can implement multiple hooks by adding a Hook attribute for each. One module can implement a hook as many times as it wants in as many classes as it wants. This allows, for example, adding form_alter implementations firing on other conditions than form id without touching any existing implementation. For now I exempted the hooks fired via ModuleHandler::invoke from this, documentation would be needed for those, luckily very few such are used runtime: library_build_info, mail and help. For example, help expects a string return, it's not even clear what multiple implementations could mean in this case. Also, ModuleHandler::invoke is used as a replacement for a failure-tolerant $function() call, once again it's not at all clear what multiple replacements could mean.
  7. All the implementations are stored in a container parameter. This is a multidimensional array with keys for hook, class, method and the values are module and priority. To reorder, use a service provider alter and change the priority. A proof of concept of how ckeditor5_module_implements_alter will look after is provided. I also added a helper method which retrieves all the priorities for a given hook to make absolute ordering easier for now. Before/after is certainly a possibility in a followup.
  8. Old style procedural calls are integrated into the new system. Separate hook caching is removed, everything is now stored in the container. I suggest contrib and custom could start converting hooks to new style and have the procedural implementation call the new style which is, again, available as a service. The attribute supports a "replaces" key for this purposes. This allows maintaining one hook implementation code while being compatible with a wide array of Drupal versions. I suppose since interfaces for autowire got added for D10.1, the floor would be D10.1 which is just as well, I believe D10.0 is out of support already.
  9. Not much code is needed and a lot of is BC: due to the vast simplification of ModuleHandler, core/lib/Drupal/Core only becomes 72 lines longer. This is enough for the new attribute, gathering all the implementation data, registering them as event subscribers and firing them as needed. Once the BC layers are removed, it'll be significantly negative. Luckily we didn't need to provide any new facility for hook_module_implements_alter() as service provider alter can do it nor any sorting because event dispatcher does that.
  10. If loading all hook classes at build time becomes a problem we already have an issue for that: #3395260: Investigate possibilities to parse attributes without reflection .
  11. For API documentation purposes, it is possible to create a separate attribute class for each and move the doxygen to the constructor. An example of such an attribute is provided in HookFormAlter (without moving the doxygen for now).
  12. For a contrib which works in Drupal 10 and 11, you will need to 1) manually register the autowired service 2) have something like \Drupal::service(NodeHook::class)->userCancel($user); as the procedural implementation 3) mark the procedural hook implementation with the [@LegacyHook] attribute. The new system will recognize LegacyHook and just not call it instead calls the new implementation directly. The rector rule for this is here.

The attached PR contains a PoC for a conversion of ckeditor alter, one node hook manually converted showing how autowire would work. I have a quick-and-dirty conversion script which eventually needs to become a rector rule but it allows interested parties to see the future.

Remaining tasks

  1. A framework manager needs to review per 61.
  2. While more tests could be written, the difficulty of getting the current test harness to pass proves there is already ample testing already. This is a decision for the community to make.

User interface changes

API changes

Oh my.

Data model changes

Release notes snippet


Viewing all articles
Browse latest Browse all 295180

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>