Verified Commit 53317130 authored by dpi's avatar dpi
Browse files

Issue #3257357: Added alter support

parent b6f0663d
Loading
Loading
Loading
Loading
+17 −9
Original line number Diff line number Diff line
@@ -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

@@ -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!'; 
  }

}
```

+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'));
  }

}
+80 −0
Original line number Diff line number Diff line
@@ -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];
  }

}
+4 −0
Original line number Diff line number Diff line
name: Hux Alter Test
type: module
description: Tests for HUX.
package: Testing
+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