Problem
The only remaining code in .module files is hook implementations.
Despite the benefit of proper namespaces in D8, the procedural function names of hook implementations can clash.
module | hook | function ------------|-------------------|------------------------ field | _group_permission | field_group_permission field_group | _permission | field_group_permission
(This is just one of many real world examples in contrib.)
ModuleHandler
even implements a (complex) concept for auto-loading hook implementations.- The Events system requires use of an Event object rather than named parameters, this creates additional boilerplate when defining events and additional levels of indirection when subscribing to events.
Proposed solution
Allow modules to provide a tagged service (tagged 'hook_subscriber') to implement hooks. This has the following advantages:
- Hook implementations can now use dependency injection
- Every hook, whether invoked by core or contrib will support the new API without any required change from contrib modules
- Over time we can add interfaces for hook implementations
The specifics could look something like this. From #43:
- - Modules define a tagged service - 'hook_subscriber'
- - (optional) weight argument to that tag to override the module weight
- - Modules can optionally for now provide MyHookInterface extends HookInterface when they invoke a hook
- - hook implementers can either annotate a method as implementing the hook, or implement MyHookInterface when available.
An example implementation:
- - Create a map at compile time (this is not worse than event subscribers):
hook => [module1:@service1, module1:@service2, module2:@service3, ...]
and store it as argument to module_handler().
Then in module handler check the map for the hook, instantiate the services from the container and call the camelized method name (or store the method, too in the annotation case.)
Retain support for legacy $module_$hook
callbacks (for now).
For that example you would do:
moderation_state.services.yml
moderation_state.entity_type
- class: Drupal\moderation_state\EntityState
- tags:
- { name: 'hook_subscriber' }
class Drupal\moderation_state\EntityState {
/**
* Implements hook_entity_type_alter().
*
* @hook entity_type_alter
*/
public function hookEntityTypeAlter(array &$entity_types) {
// ...
}
}
And optionally for e.g. best practice (if the module supports it) you would have interfaces available:
use Drupal\entity\Hook\HookEntityTypeAlterInterface;
use Drupal\entity\Hook\HookEntityOperationInterface;
use Drupal\entity\Hook\HookEntityBundleFieldInfoAlterInterface;
class Drupal\moderation_state\EntityState implements HookEntityTypeAlterInterface, HookEntityOperationInterface, HookEntityBundleFieldInfoAlterInterface{
/**
* {@inheritdoc}
*/
public function hookEntityTypeAlter(array &$entity_types) {
// ...
}
/**
* {@inheritdoc}
*/
public function hookEntityOperation(EntityInterface $entity) {
// ...
}
/**
* {@inheritdoc}
*/
public function hookEntityBundleFieldInfoAlter(&$fields, EntityTypeInterface $entity_type, $bundle) {
// ...
}
}
That proposal would continue to work with dynamic hooks like hook_form_FORMID_alter.
it would just be called:
hookFormMyFormAlter() what normally would be my_module_form_my_form_alter().
Obviously you can't add an interface for that so it would need to rely on the @hook annotation.
Remaining tasks
#2616814: Delegate all hook invocations to ModuleHandler
#2264047: [meta] Document service definition tags