Skip to content
Snippets Groups Projects
Commit 5b820c8a authored by dpi's avatar dpi
Browse files

Issue #3261781 by dpi: Associate method with multiple hooks/alters

parent bd330cd4
No related branches found
No related tags found
No related merge requests found
......@@ -6,11 +6,11 @@ Hux is a project specifically designed for developers, allowing hook
implementations without needing to define a .module file or any kind of proxy
class/service features.
There are a [few][project-hook_event_dispatcher] [projects][project-hooks]
There are a [few][project-hook_event_dispatcher] [projects][project-hooks]
[out][project-entity_events] there that try to introduce an event subscriber
driven way of working.
driven way of working.
Hux is an in between solution, allowing the full benefits of dependency
injection and class driven logic without going fully in with events.
injection and class driven logic without going fully in with events.
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.
......@@ -33,7 +33,7 @@ Other features
Add an entry to your modules' services.yml file. The entry simply needs to be a
public service, with a class and the 'hooks' tag.
Once a hook class has been added as a service, just clear the site cache.
Once a hook class has been added as a service, just clear the site cache.
Tip: You do not need to clear the site cache to add more hook implementations!
......@@ -57,36 +57,36 @@ use Drupal\hux\Attribute\Hook;
use Drupal\hux\Attribute\ReplaceOriginalHook;
/**
* Examples of 'entity_access' hooks and 'user_format_name' alter.
* Usage examples.
*/
final class MyModuleHooks {
#[Hook('entity_access')]
public function myEntityAccess(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult {
public function myEntityAccess(EntityInterface $entity, string $operation, AccountInterface $account): AccessResultInterface {
// A barebones implementation.
return AccessResult::neutral();
}
#[Hook('entity_access', priority: 100)]
public function myEntityAccess2(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult {
public function myEntityAccess2(EntityInterface $entity, string $operation, AccountInterface $account): AccessResultInterface {
// 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(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult {
public function myEntityAccess3(EntityInterface $entity, string $operation, AccountInterface $account): AccessResultInterface {
// You can masquerade as a different module!
return AccessResult::neutral();
}
#[ReplaceOriginalHook(hook: 'entity_access', moduleName: 'media')]
public function myEntityAccess4(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult {
public function myEntityAccess4(EntityInterface $entity, string $operation, AccountInterface $account): AccessResultInterface {
// 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, string $operation, AccountInterface $account): AccessResult {
public function myEntityAccess5(callable $originalInvoker, EntityInterface $entity, string $operation, AccountInterface $account): AccessResultInterface {
// 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);
......@@ -96,7 +96,17 @@ final class MyModuleHooks {
#[Alter('user_format_name')]
public function myCustomAlter(string &$name, AccountInterface $account): void {
$name .= ' altered!';
$name .= ' altered!';
}
#[
Hook('entity_insert'),
Hook('entity_delete'),
]
public function myEntityAccess(EntityInterface $entity): void {
// Associate with multiple!
// Also works with Alters and Replacements.
return AccessResult::neutral();
}
}
......@@ -112,7 +122,7 @@ Working examples of all Hux features can be found in included tests.
## Optimised mode
Hux' [optimized mode][optimized-mode] provides an option geared towards being
Hux' [optimized mode][optimized-mode] provides an option geared towards being
developer friendly or optimized for production use. By default this mode is off,
but it should be turned on in production for small gains in performance.
......
......@@ -7,7 +7,7 @@ namespace Drupal\hux\Attribute;
/**
* An alter.
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
// @codingStandardsIgnoreLine
class Alter {
......
......@@ -7,7 +7,7 @@ namespace Drupal\hux\Attribute;
/**
* A hook.
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
// @codingStandardsIgnoreLine
class Hook {
......
......@@ -14,7 +14,7 @@ namespace Drupal\hux\Attribute;
*
* This does not extend the Hook attribute to simplify things.
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
// @codingStandardsIgnoreLine
final class ReplaceOriginalHook {
......
......@@ -65,7 +65,7 @@ final class HuxDiscovery {
$methodName = $reflectionMethod->getName();
$attributesHooks = $reflectionMethod->getAttributes(Hook::class);
if ($attribute = $attributesHooks[0] ?? NULL) {
foreach ($attributesHooks as $attribute) {
$instance = $attribute->newInstance();
assert($instance instanceof Hook);
$this->discovery[Hook::class][$instance->hook][] = [
......@@ -77,7 +77,7 @@ final class HuxDiscovery {
}
$attributesHookReplacements = $reflectionMethod->getAttributes(ReplaceOriginalHook::class);
if ($attribute = $attributesHookReplacements[0] ?? NULL) {
foreach ($attributesHookReplacements as $attribute) {
$instance = $attribute->newInstance();
assert($instance instanceof ReplaceOriginalHook);
$this->discovery[ReplaceOriginalHook::class][$instance->hook][] = [
......@@ -89,7 +89,7 @@ final class HuxDiscovery {
}
$attributesAlters = $reflectionMethod->getAttributes(Alter::class);
if ($attribute = $attributesAlters[0] ?? NULL) {
foreach ($attributesAlters as $attribute) {
$instance = $attribute->newInstance();
assert($instance instanceof Alter);
$this->discovery[Alter::class][$instance->alter][] = [
......
......@@ -30,4 +30,19 @@ final class HuxAlterTestHooks {
$data = __FUNCTION__ . ' hit';
}
/**
* Implements test_hook_multi_listener_alter().
* Implements test_hook_multi_listener2_alter().
*
* Tests an alter listening for multiple hooks.
*/
#[
Alter('multi_listener'),
Alter('multi_listener2'),
]
public function testAlterMultiListener(&$data, &$context1, &$context2): void {
$context1++;
HuxTestCallTracker::record([__CLASS__, __FUNCTION__, $data, $context1, $context2]);
}
}
......@@ -35,4 +35,18 @@ final class HuxReplacementTestHooks {
return __FUNCTION__ . ' return';
}
/**
* Replaces hux_test_foo3().
* Replaces hux_test_foo4().
*
* Tests a hook replacement listening for multiple hooks.
*/
#[
ReplaceOriginalHook('foo3', moduleName: 'hux_test'),
ReplaceOriginalHook('foo4', moduleName: 'hux_test'),
]
public function testHookMultiListener(string $something): void {
HuxTestCallTracker::record([__CLASS__, __FUNCTION__, $something]);
}
}
......@@ -22,3 +22,19 @@ function hux_test_foo2(): string {
HuxTestCallTracker::record(__FUNCTION__);
return __FUNCTION__ . ' return';
}
/**
* Implements hook_foo3().
*/
function hux_test_foo3(): string {
HuxTestCallTracker::record(__FUNCTION__);
return __FUNCTION__ . ' return';
}
/**
* Implements hook_foo4().
*/
function hux_test_foo4(): string {
HuxTestCallTracker::record(__FUNCTION__);
return __FUNCTION__ . ' return';
}
......@@ -32,4 +32,18 @@ final class HuxTestHooks {
return __FUNCTION__ . ' return';
}
/**
* Implements test_hook_multi_listener().
* Implements test_hook_multi_listener2().
*
* Tests a hook listening for multiple hooks.
*/
#[
Hook('test_hook_multi_listener'),
Hook('test_hook_multi_listener2'),
]
public function testHookMultiListener(string $something): void {
HuxTestCallTracker::record([__CLASS__, __FUNCTION__, $something]);
}
}
......@@ -95,6 +95,38 @@ final class HuxAlterTest extends KernelTestBase {
$this->assertEquals('context two', $context2);
}
/**
* @covers \Drupal\hux\HuxDiscovery::discovery
* @see \Drupal\hux_alter_test\HuxAlterTestHooks::testAlterMultiListener
*/
public function testMultiListener(): void {
$data = __FUNCTION__;
// Treat as a counter.
$context1 = 0;
$context2 = 'context two';
$this->moduleHandler()->alter([
'multi_listener',
'multi_listener2',
], $data, $context1, $context2);
$this->assertEquals([
[
'Drupal\hux_alter_test\HuxAlterTestHooks',
'testAlterMultiListener',
'testMultiListener',
1,
'context two',
],
[
'Drupal\hux_alter_test\HuxAlterTestHooks',
'testAlterMultiListener',
'testMultiListener',
2,
'context two',
],
], HuxTestCallTracker::$calls);
}
/**
* The module installer.
*/
......
......@@ -259,6 +259,29 @@ final class HuxReplacementTest extends KernelTestBase {
], $result);
}
/**
* Tests a replacement of multiple hooks.
*
* @covers \Drupal\hux\HuxDiscovery::discovery
*/
public function testMultiReplacement(): void {
$this->moduleInstaller()->install(['hux_replacement_test'], TRUE);
$this->moduleHandler()->invokeAll('foo3', ['bar1']);
$this->moduleHandler()->invokeAll('foo4', ['bar2']);
$this->assertEquals([
[
'Drupal\hux_replacement_test\HuxReplacementTestHooks',
'testHookMultiListener',
'bar1',
],
[
'Drupal\hux_replacement_test\HuxReplacementTestHooks',
'testHookMultiListener',
'bar2',
],
], HuxTestCallTracker::$calls);
}
/**
* The module installer.
*/
......
......@@ -61,6 +61,28 @@ final class HuxTest extends KernelTestBase {
], $result);
}
/**
* @covers \Drupal\hux\HuxDiscovery::discovery
* @see \Drupal\hux_test\HuxTestHooks::testHookMultiListener
*/
public function testMultiListener(): void {
$this->moduleHandler()->invokeAll('test_hook_multi_listener', ['bar1']);
$this->moduleHandler()->invokeAll('test_hook_multi_listener2', ['bar2']);
$this->assertEquals([
[
'Drupal\hux_test\HuxTestHooks',
'testHookMultiListener',
'bar1',
],
[
'Drupal\hux_test\HuxTestHooks',
'testHookMultiListener',
'bar2',
],
], HuxTestCallTracker::$calls);
$this->moduleHandler()->invokeAll('test_hook', ['bar']);
}
/**
* The module installer.
*/
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment