Loading README.md +17 −9 Original line number Diff line number Diff line Loading @@ -15,10 +15,12 @@ Methods can have the same signature as original hook implementations. Discovery is automatic, only requiring a hook class to be registered as a tagged Drupal service and initial cache clear. You can also define multiple hook implementation per module! Other features Overriding original hook implementations is also possible using the - Multiple hook implementations per module! - Overriding original hook implementations is possible using the `[#ReplaceOriginalHook]` annotation. - Supports alters. # Installation Loading Loading @@ -50,40 +52,41 @@ declare(strict_types=1); namespace Drupal\my_module; use Drupal\hux\Attribute\Alter; use Drupal\hux\Attribute\Hook; use Drupal\hux\Attribute\ReplaceOriginalHook; /** * Examples of 'entity_access' hooks. * Examples of 'entity_access' hooks and 'user_format_name' alter. */ final class MyModuleHooks { #[Hook('entity_access')] public function myEntityAccess($entity, $operation, $account): AccessResult { public function myEntityAccess(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // A barebones implementation. return AccessResult::neutral(); } #[Hook('entity_access', priority: 100)] public function myEntityAccess2($entity, $operation, $account): AccessResult { public function myEntityAccess2(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // You can set priority if you have multiple of the same hook! return AccessResult::neutral(); } #[Hook('entity_access', moduleName: 'a_different_module', priority: 200)] public function myEntityAccess3($entity, $operation, $account): AccessResult { public function myEntityAccess3(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // You can masquerade as a different module! return AccessResult::neutral(); } #[ReplaceOriginalHook(hook: 'entity_access', moduleName: 'media')] public function myEntityAccess4(EntityInterface $entity, $operation, AccountInterface $account): AccessResult { public function myEntityAccess4(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // You can override hooks for other modules! E.g \media_entity_access() return AccessResult::neutral(); } #[ReplaceOriginalHook(hook: 'entity_access', moduleName: 'media', originalInvoker: TRUE)] public function myEntityAccess5(callable $originalInvoker, EntityInterface $entity, $operation, AccountInterface $account): AccessResult { public function myEntityAccess5(callable $originalInvoker, EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // If you override a hook for another module, you can have the original // implementation passed to you as a callable! $originalResult = $originalInvoker($entity, $operation, $account); Loading @@ -91,6 +94,11 @@ final class MyModuleHooks { return AccessResult::neutral(); } #[Alter('user_format_name')] public function myEntityAccess3(string &$name, AccountInterface $account): AccessResult { $name .= ' altered!'; } } ``` Loading src/Attribute/Alter.php 0 → 100644 +26 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\hux\Attribute; /** * An alter. */ #[\Attribute] class Alter { /** * Constructs a new Alter. * * @param string $alter * The alter name, without the 'hook_' or '_alter' components. */ public function __construct( public string $alter, ) { assert(!str_starts_with($alter, 'hook_')); assert(!str_ends_with($alter, '_alter')); } } src/HuxModuleHandler.php +80 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ namespace Drupal\hux; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\hux\Attribute\Alter; use Drupal\hux\Attribute\Hook; use Drupal\hux\Attribute\ReplaceOriginalHook; use Symfony\Component\DependencyInjection\ContainerAwareTrait; Loading Loading @@ -44,6 +45,13 @@ final class HuxModuleHandler implements ModuleHandlerInterface { */ private array $hookReplacements; /** * Alter callables keyed by alter. * * @var array<string, callable[]> */ private array $alters; /** * Constructs a new HuxModuleHandler. * Loading Loading @@ -153,6 +161,34 @@ final class HuxModuleHandler implements ModuleHandlerInterface { } } /** * {@inheritdoc} */ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL): void { $this->inner->alter($type, $data, $context1, $context2); $types = is_array($type) ? $type : [$type]; foreach ($types as $alter) { foreach ($this->generateAlterInvokers($alter) as $alterInvoker) { $alterInvoker($data, $context1, $context2); } } } /** * {@inheritdoc} */ public function alterDeprecated($description, $type, &$data, &$context1 = NULL, &$context2 = NULL): void { $this->inner->alterDeprecated($description, $type, $data, $context1, $context2); $types = is_array($type) ? $type : [$type]; foreach ($types as $alter) { foreach ($this->generateAlterInvokers($alter) as $alterInvoker) { $alterInvoker($data, $context1, $context2); } } } /** * Generates invokers for a hook. * Loading Loading @@ -249,4 +285,48 @@ final class HuxModuleHandler implements ModuleHandlerInterface { return $this->hookReplacements[$hook]; } /** * Generates invokers for an alter. * * @param string $alter * An alter. * * @return \Generator<array{callable, string}> * A generator with hook callbacks and other metadata. */ private function generateAlterInvokers(string $alter) { if (isset($this->alters[$alter])) { yield from $this->alters[$alter]; return; } $alters = []; foreach ($this->implementations as [$serviceId, $moduleName]) { $service = $this->container->get($serviceId); $reflectionClass = new \ReflectionClass($service); $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($methods as $reflectionMethod) { $attributes = $reflectionMethod->getAttributes(Alter::class); $attribute = $attributes[0] ?? NULL; if ($attribute) { $instance = $attribute->newInstance(); assert($instance instanceof Alter); if ($alter === $instance->alter) { $alters[] = \Closure::fromCallable([ $service, $reflectionMethod->getName(), ]); } } } } // Wait for all the [sorted] callables before caching. $this->alters[$alter] = $alters; yield from $this->alters[$alter]; } } tests/modules/hux_alter_test/hux_alter_test.info.yml 0 → 100644 +4 −0 Original line number Diff line number Diff line name: Hux Alter Test type: module description: Tests for HUX. package: Testing tests/modules/hux_alter_test/hux_alter_test.module 0 → 100644 +24 −0 Original line number Diff line number Diff line <?php /** * @file * Hooks for Hux Alter Test. */ use Drupal\hux_test\HuxTestCallTracker; /** * Implements hook_fizz_alter(). */ function hux_alter_test_fizz_alter(&$data, &$context1, &$context2): void { HuxTestCallTracker::record(__FUNCTION__); $data = __FUNCTION__ . ' hit'; } /** * Implements hook_buzz_alter(). */ function hux_alter_test_buzz_alter(&$data, &$context1, &$context2): void { HuxTestCallTracker::record(__FUNCTION__); $data = __FUNCTION__ . ' hit'; } Loading
README.md +17 −9 Original line number Diff line number Diff line Loading @@ -15,10 +15,12 @@ Methods can have the same signature as original hook implementations. Discovery is automatic, only requiring a hook class to be registered as a tagged Drupal service and initial cache clear. You can also define multiple hook implementation per module! Other features Overriding original hook implementations is also possible using the - Multiple hook implementations per module! - Overriding original hook implementations is possible using the `[#ReplaceOriginalHook]` annotation. - Supports alters. # Installation Loading Loading @@ -50,40 +52,41 @@ declare(strict_types=1); namespace Drupal\my_module; use Drupal\hux\Attribute\Alter; use Drupal\hux\Attribute\Hook; use Drupal\hux\Attribute\ReplaceOriginalHook; /** * Examples of 'entity_access' hooks. * Examples of 'entity_access' hooks and 'user_format_name' alter. */ final class MyModuleHooks { #[Hook('entity_access')] public function myEntityAccess($entity, $operation, $account): AccessResult { public function myEntityAccess(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // A barebones implementation. return AccessResult::neutral(); } #[Hook('entity_access', priority: 100)] public function myEntityAccess2($entity, $operation, $account): AccessResult { public function myEntityAccess2(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // You can set priority if you have multiple of the same hook! return AccessResult::neutral(); } #[Hook('entity_access', moduleName: 'a_different_module', priority: 200)] public function myEntityAccess3($entity, $operation, $account): AccessResult { public function myEntityAccess3(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // You can masquerade as a different module! return AccessResult::neutral(); } #[ReplaceOriginalHook(hook: 'entity_access', moduleName: 'media')] public function myEntityAccess4(EntityInterface $entity, $operation, AccountInterface $account): AccessResult { public function myEntityAccess4(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // You can override hooks for other modules! E.g \media_entity_access() return AccessResult::neutral(); } #[ReplaceOriginalHook(hook: 'entity_access', moduleName: 'media', originalInvoker: TRUE)] public function myEntityAccess5(callable $originalInvoker, EntityInterface $entity, $operation, AccountInterface $account): AccessResult { public function myEntityAccess5(callable $originalInvoker, EntityInterface $entity, string $operation, AccountInterface $account): AccessResult { // If you override a hook for another module, you can have the original // implementation passed to you as a callable! $originalResult = $originalInvoker($entity, $operation, $account); Loading @@ -91,6 +94,11 @@ final class MyModuleHooks { return AccessResult::neutral(); } #[Alter('user_format_name')] public function myEntityAccess3(string &$name, AccountInterface $account): AccessResult { $name .= ' altered!'; } } ``` Loading
src/Attribute/Alter.php 0 → 100644 +26 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\hux\Attribute; /** * An alter. */ #[\Attribute] class Alter { /** * Constructs a new Alter. * * @param string $alter * The alter name, without the 'hook_' or '_alter' components. */ public function __construct( public string $alter, ) { assert(!str_starts_with($alter, 'hook_')); assert(!str_ends_with($alter, '_alter')); } }
src/HuxModuleHandler.php +80 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ namespace Drupal\hux; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\hux\Attribute\Alter; use Drupal\hux\Attribute\Hook; use Drupal\hux\Attribute\ReplaceOriginalHook; use Symfony\Component\DependencyInjection\ContainerAwareTrait; Loading Loading @@ -44,6 +45,13 @@ final class HuxModuleHandler implements ModuleHandlerInterface { */ private array $hookReplacements; /** * Alter callables keyed by alter. * * @var array<string, callable[]> */ private array $alters; /** * Constructs a new HuxModuleHandler. * Loading Loading @@ -153,6 +161,34 @@ final class HuxModuleHandler implements ModuleHandlerInterface { } } /** * {@inheritdoc} */ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL): void { $this->inner->alter($type, $data, $context1, $context2); $types = is_array($type) ? $type : [$type]; foreach ($types as $alter) { foreach ($this->generateAlterInvokers($alter) as $alterInvoker) { $alterInvoker($data, $context1, $context2); } } } /** * {@inheritdoc} */ public function alterDeprecated($description, $type, &$data, &$context1 = NULL, &$context2 = NULL): void { $this->inner->alterDeprecated($description, $type, $data, $context1, $context2); $types = is_array($type) ? $type : [$type]; foreach ($types as $alter) { foreach ($this->generateAlterInvokers($alter) as $alterInvoker) { $alterInvoker($data, $context1, $context2); } } } /** * Generates invokers for a hook. * Loading Loading @@ -249,4 +285,48 @@ final class HuxModuleHandler implements ModuleHandlerInterface { return $this->hookReplacements[$hook]; } /** * Generates invokers for an alter. * * @param string $alter * An alter. * * @return \Generator<array{callable, string}> * A generator with hook callbacks and other metadata. */ private function generateAlterInvokers(string $alter) { if (isset($this->alters[$alter])) { yield from $this->alters[$alter]; return; } $alters = []; foreach ($this->implementations as [$serviceId, $moduleName]) { $service = $this->container->get($serviceId); $reflectionClass = new \ReflectionClass($service); $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($methods as $reflectionMethod) { $attributes = $reflectionMethod->getAttributes(Alter::class); $attribute = $attributes[0] ?? NULL; if ($attribute) { $instance = $attribute->newInstance(); assert($instance instanceof Alter); if ($alter === $instance->alter) { $alters[] = \Closure::fromCallable([ $service, $reflectionMethod->getName(), ]); } } } } // Wait for all the [sorted] callables before caching. $this->alters[$alter] = $alters; yield from $this->alters[$alter]; } }
tests/modules/hux_alter_test/hux_alter_test.info.yml 0 → 100644 +4 −0 Original line number Diff line number Diff line name: Hux Alter Test type: module description: Tests for HUX. package: Testing
tests/modules/hux_alter_test/hux_alter_test.module 0 → 100644 +24 −0 Original line number Diff line number Diff line <?php /** * @file * Hooks for Hux Alter Test. */ use Drupal\hux_test\HuxTestCallTracker; /** * Implements hook_fizz_alter(). */ function hux_alter_test_fizz_alter(&$data, &$context1, &$context2): void { HuxTestCallTracker::record(__FUNCTION__); $data = __FUNCTION__ . ' hit'; } /** * Implements hook_buzz_alter(). */ function hux_alter_test_buzz_alter(&$data, &$context1, &$context2): void { HuxTestCallTracker::record(__FUNCTION__); $data = __FUNCTION__ . ' hit'; }