Problem/Motivation
Currently in Drupal we have the old procedural hook system, we have symfony events, and we have tagged services and service collectors, to allow different modules or components broadcast or respond to "events" (in the broader sense).
The procedural hook system has a number of problems and limitations (some already mentioned in #2237831):
- Possibility of name clashes / ambiguity, e.g. "field" + "_group_permission" vs "field_group" + "_permission".
- No dependency injection: Hook implementations have to call
\Drupal::service(...)
to get their dependencies. - Most implementations are in *.module files, which can get crowded, make it hard to organize code, and can easily cause git merge conflicts, if multiple branches attempt to add new hook implementations.
- There can be only one implementation per module (or the module has to "cheat", and pretend it is implementing on behalf of another module, one that might not even exist).
- The procedural code feels out of place compared to the rest of Drupal.
Alternatives:
- symfony events require a dedicated event class per event type instead of named parameters, which "creates additional boilerplate when defining events and additional levels of indirection when subscribing to events." (quote from #2237831). See #2551893: Add events for matching entity hooks
- tagged services and services collectors typically require a dedicated interface per "event type", and subscribers (= tagged services) to implement that interface.
None of these "alternatives" is a good candidate to fully and generically convert the existing hook system. All past attempts to do so (that I am aware of) only focused on very specific hooks, or families of hooks.
Proposed resolution
The Hux module allows to mark methods on services classes as hook implementation, using php attributes.
Something similar should be added in Drupal core, so that existing hook implementations can be converted to OOP code.
This seems to be the current consensus in #2237831: Allow module services to specify hooks.
class MyService {
#[Hook('node_insert'), Hook('node_update')]
public function nodeWrite(NodeInterface $node): void {..}
#[Alter('form_node_edit_form')]
public function nodeFormAlter(array $form, FormStateInterface $form_state): void {..}
}
Goals / requirements
Backwards support:
- The first iteration of this should be fully compatible with existing procedural hook implementations and also on the calling code side.
- There will be a mix of procedural and non-procedural implementations.
- The old
hook_module_implements_alter()
will still work on the list of procedural implementations, but a new hook (or event?) could be added to reorder or alter the collection that includes OOP implementations. - The behavior and return value of any call to $module_handler->invoke() or $module_handler->invokeAll() etc...
- ...must remain unchanged, if no new implementations were added.
- ...must remain unchanged, if existing implementations are merely converted to the new system.
- ...must only gradually change, if new implementations are added using the new system.
- Modules can start implementing existing hooks using the new system, without any change in the module that publishes or invokes the hook.
Basic new features:
- Methods on service classes can subscribe to hooks using attributes like
#[Hook('node_update')]
. - One method can implement more than one hook.
- Multiple methods in the same module can implement the same hook, resulting in multiple implementations per module.
This introduces some challenges with return value of single$module_handler->invoke(..)
, see below. - There must be a mechanism to order different implementations. E.g. in hux there is a system of weights / priorities, but in core we might do something different.
- There must be a mechanism to replace or remove existing implementations.
- For the two requirements above, it might be necessary to have unique machine names for individual implementations.
Optional or advanced features:
- There can be a discovery mechanism to register classes with hooks as services. E.g. in hux, this happens if these classes are in a
Drupal\\$modulename\\Hooks
namespace. - Modules can add additional attribute classes, to let the method implement one or more hooks. E.g.
#[HookEntityOp(['node', 'taxonomy_term'], ['update', 'insert'])]
would implement 4 different hooks. - Modules can add other callbacks as hook implementations, that don't rely on php attributes, and that can be added dynamically.
- We might add additional methods to ModuleHandler, with more control of the results aggregation for single ->invoke(). See "Challenges" below.
- We might add hooks (or anoother mechanism) to control the default results aggregation per hook name.
Challenges
- Return value of single
$module_handler->invoke()
:- Currently, it returns the return value of the one single implementation. The behavior for this case must not change!
- With multiple implementations that return arrays, e.g. for
hook_schema()
, we would expect the results to be merged. Any null values would be skipped, if the "collected data" is already an array. - For more than one non-array non-null values, or a mix of array and non-array values, an exception could be thrown, because these cannot be merged, unless an aggregation method is specified somewhere.
- Design a system to define order of implementations, and to remove or replace implementations. Options:
- Before/after rules, as proposed for events in #2352351: Add before/after ordering to events
- Numeric weights/priorities, as currently in hux.
- A hook or similar to alter the list of implementations, similar to hook_module_implements_alter(), but with support for callbacks.
- Extending hook_module_implements_alter() in a BC-friendly way. This could be tricky.
Underlying architecture
The module handler ->invoke(), ->invokeAll() etc should operate with a list of callbacks, that can come from different sources.
The methods with attributes will be one such source, but it should be flexible enough to support other sources.
Proposed roadmap / target versions
- Study and play with the hux module as a proof of concept.
- Build the new system in latest version of Drupal 10 (10.2.x). Do not change the existing implementations and calling code, yet.
This allows contrib to start using the new system, while minimizing disruption from upgrading to the new core version.
Any possible BC conflict will only apply if there is a module that uses the new system. - Start converting existing implementations in a future version - 10.? or 11 - depends how disruptive this turns out to be.
- Add additional changes that are not backwards compatible, consider to deprecate procedural hooks, in Drupal 11.
Remaining tasks
User interface changes
API changes
Methods in service classes can be marked as hook implementations using attributes.
Details are provided above.
Data model changes
Caches for the new kinds of implementations and discovery data.