Skip to content
Snippets Groups Projects
Verified Commit 53317130 authored by dpi's avatar dpi
Browse files

Issue #3257357: Added alter support

parent b6f0663d
No related branches found
No related tags found
No related merge requests found
......@@ -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
`[#ReplaceOriginalHook]` annotation.
- Multiple hook implementations per module!
- Overriding original hook implementations is possible using the
`[#ReplaceOriginalHook]` annotation.
- Supports alters.
# Installation
......@@ -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);
......@@ -91,6 +94,11 @@ final class MyModuleHooks {
return AccessResult::neutral();
}
#[Alter('user_format_name')]
public function myEntityAccess3(string &$name, AccountInterface $account): AccessResult {
$name .= ' altered!';
}
}
```
......
<?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'));
}
}
......@@ -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;
......@@ -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.
*
......@@ -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.
*
......@@ -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];
}
}
name: Hux Alter Test
type: module
description: Tests for HUX.
package: Testing
<?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';
}
services:
hux_alter_test.hooks:
class: Drupal\hux_alter_test\HuxAlterTestHooks
tags:
- { name: hooks, priority: 100 }
<?php
declare(strict_types=1);
namespace Drupal\hux_alter_test;
use Drupal\hux\Attribute\Alter;
use Drupal\hux_test\HuxTestCallTracker;
final class HuxAlterTestHooks {
/**
* Implements hook_fizz_alter().
*/
#[Alter('fizz')]
public function testAlter1(&$data, &$context1, &$context2): void {
HuxTestCallTracker::record([__CLASS__, __FUNCTION__, $data, $context1, $context2]);
$data = __FUNCTION__ . ' hit';
}
/**
* Implements hook_buzz_alter().
*/
#[Alter('buzz')]
public function testAlter2(&$data, &$context1, &$context2): void {
HuxTestCallTracker::record([__CLASS__, __FUNCTION__, $data, $context1, $context2]);
$data = __FUNCTION__ . ' hit';
}
}
<?php
declare(strict_types=1);
namespace Drupal\Tests\hux\Kernel;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\hux_test\HuxTestCallTracker;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests alters.
*
* @group hux
* @coversDefaultClass \Drupal\hux\HuxModuleHandler
*/
final class HuxAlterTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'hux',
'hux_test',
'hux_alter_test',
];
/**
* Tests alter invokation.
*
* @covers ::alter
* @see \Drupal\hux_alter_test\HuxAlterTestHooks::testAlter1
* @see \hux_alter_test_fizz_alter
*/
public function testAlterTypeIsString(): void {
$data = __FUNCTION__;
$context1 = 'context one';
$context2 = 'context two';
$this->moduleHandler()->alter('fizz', $data, $context1, $context2);
$this->assertEquals([
'hux_alter_test_fizz_alter',
[
'Drupal\hux_alter_test\HuxAlterTestHooks',
'testAlter1',
'hux_alter_test_fizz_alter hit',
'context one',
'context two',
],
], HuxTestCallTracker::$calls);
$this->assertEquals('testAlter1 hit', $data);
$this->assertEquals('context one', $context1);
$this->assertEquals('context two', $context2);
}
/**
* Tests alter invokation.
*
* @covers ::alter
* @see \Drupal\hux_alter_test\HuxAlterTestHooks::testAlter1
* @see \Drupal\hux_alter_test\HuxAlterTestHooks::testAlter2
* @see \hux_alter_test_fizz_alter
* @see \hux_alter_test_buzz_alter
*/
public function testAlterTypeIsArray(): void {
$data = __FUNCTION__;
$context1 = 'context one';
$context2 = 'context two';
$this->moduleHandler()->alter([
'fizz',
'buzz',
], $data, $context1, $context2);
$this->assertEquals([
'hux_alter_test_fizz_alter',
'hux_alter_test_buzz_alter',
[
'Drupal\hux_alter_test\HuxAlterTestHooks',
'testAlter1',
'hux_alter_test_buzz_alter hit',
'context one',
'context two',
],
[
'Drupal\hux_alter_test\HuxAlterTestHooks',
'testAlter2',
'testAlter1 hit',
'context one',
'context two',
],
], HuxTestCallTracker::$calls);
$this->assertEquals('testAlter2 hit', $data);
$this->assertEquals('context one', $context1);
$this->assertEquals('context two', $context2);
}
/**
* The module installer.
*/
private function moduleInstaller(): ModuleInstallerInterface {
return \Drupal::service('module_installer');
}
/**
* The module handler.
*/
private function moduleHandler(): ModuleHandlerInterface {
return \Drupal::service('module_handler');
}
}
......@@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Drupal\Tests\hux\Kernel;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\hux_test\HuxTestCallTracker;
......
......@@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Drupal\Tests\hux\Kernel;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\hux_test\HuxTestCallTracker;
......
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