Verified Commit bdf4b395 authored by Andrei Mateescu's avatar Andrei Mateescu
Browse files

task: #3583153 Allow to invoke multiple implementations of a specific module...

task: #3583153 Allow to invoke multiple implementations of a specific module with ModuleHandler::invoke()

By: nicxvan
By: berdir
By: smustgrave
By: catch
By: amateescu
(cherry picked from commit ea517b5b)
parent e5dd1330
Loading
Loading
Loading
Loading
+14 −1
Changes for core/lib/Drupal/Core/Extension/ModuleHandler.php: 14 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -349,7 +349,20 @@ public function invoke($module, $hook, array $args = []) {
    $listeners = $list->getForModule($module);
    if ($listeners) {
      if (count($listeners) > 1) {
        throw new \LogicException("Module $module should not implement $hook more than once");
        $results = [];
        foreach ($listeners as $listener) {
          $result = $listener(...$args);
          if ($result === NULL) {
            continue;
          }
          if (!is_array($result) && !($result instanceof \ArrayObject)) {
            // If the result is not an array and there is more than one
            // implementation for the module we cannot merge the results.
            throw new \LogicException("Module $module should not implement $hook more than once.");
          }
          $results = NestedArray::mergeDeep($results, $result);
        }
        return $results;
      }
      return reset($listeners)(... $args);
    }
+3 −0
Changes for core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -242,6 +242,9 @@ public function invokeAllWith(string $hook, callable $callback): void;
  /**
   * Invokes a hook in a particular module.
   *
   * This will execute multiple implementations as long as all returned values
   * are void or an array. Arrays will be merged with NestedArray::mergeDeep.
   *
   * @param string $module
   *   The name of the module (without the .module extension).
   * @param string $hook
+7 −0
Changes for core/modules/system/tests/modules/hook_single_invoke/hook_single_invoke.info.yml: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
name: Hook test single module invokes
type: module
description: 'Test module used to test module handler invoke.'
package: Testing
version: VERSION
core_version_requirement: '*'
hidden: true
+84 −0
Changes for core/modules/system/tests/modules/hook_single_invoke/src/Hook/TestHookInvoke.php: 84 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\hook_single_invoke\Hook;

use Drupal\Core\Hook\Attribute\Hook;

/**
 * Add four hook implementations for two hooks, one returns arrays one string.
 */
class TestHookInvoke {

  /**
   * This hook returns an array.
   */
  #[Hook('custom_hook_invoke_array')]
  public function hookInvokeSingleArrayOne(): array {
    return [__METHOD__];
  }

  /**
   * This hook returns an array.
   */
  #[Hook('custom_hook_invoke_array')]
  public function hookInvokeSingleArrayTwo(): array {
    return [__METHOD__];
  }

  /**
   * This hook returns an ArrayObject.
   *
   * @return \ArrayObject< int, string >
   *   ArrayObject of the method.
   */
  #[Hook('custom_hook_invoke_array_object')]
  public function hookInvokeSingleArrayObjectOne(): \ArrayObject {
    return new \ArrayObject([__METHOD__]);
  }

  /**
   * This hook returns an ArrayObject.
   *
   * @return \ArrayObject< int, string >
   *   ArrayObject of the method.
   */
  #[Hook('custom_hook_invoke_array_object')]
  public function hookInvokeSingleArrayObjectTwo(): \ArrayObject {
    return new \ArrayObject([__METHOD__]);
  }

  /**
   * This hook returns a string.
   */
  #[Hook('custom_hook_invoke_string')]
  public function hookInvokeSingleStringOne(): string {
    return __METHOD__;
  }

  /**
   * This hook returns a string.
   */
  #[Hook('custom_hook_invoke_string')]
  public function hookInvokeSingleStringTwo(): string {
    return __METHOD__;
  }

  /**
   * This hook returns a stdClass.
   */
  #[Hook('custom_hook_invoke_class')]
  public function hookInvokeSingleClassOne(): \stdClass {
    return new \stdClass();
  }

  /**
   * This hook returns a stdClass.
   */
  #[Hook('custom_hook_invoke_class')]
  public function hookInvokeSingleClassTwo(): \stdClass {
    return new \stdClass();
  }

}
+43 −0
Changes for core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php: 43 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -398,6 +398,49 @@ public function testOopPreprocess(): void {
    }
  }

  /**
   * Tests invoke works for multiple implementations that can be merged.
   */
  public function testInvokeWithMergeable(): void {
    $this->moduleInstaller()->install(['hook_single_invoke']);

    $expected = [
      'Drupal\hook_single_invoke\Hook\TestHookInvoke::hookInvokeSingleArrayOne',
      'Drupal\hook_single_invoke\Hook\TestHookInvoke::hookInvokeSingleArrayTwo',
    ];
    $this->assertEquals($expected, $this->moduleHandler()->invoke('hook_single_invoke', 'custom_hook_invoke_array', args: [TRUE]));

    $expected = [
      'Drupal\hook_single_invoke\Hook\TestHookInvoke::hookInvokeSingleArrayObjectOne',
      'Drupal\hook_single_invoke\Hook\TestHookInvoke::hookInvokeSingleArrayObjectTwo',
    ];
    $this->assertEquals($expected, $this->moduleHandler()->invoke('hook_single_invoke', 'custom_hook_invoke_array_object', args: [TRUE]));
  }

  /**
   * Tests invoke fails for multiple implementations that cannot be merged.
   */
  public function testInvokeWithNotMergeableString(): void {
    $this->moduleInstaller()->install(['hook_single_invoke']);

    $expected_exception_message = 'Module hook_single_invoke should not implement custom_hook_invoke_string more than once.';
    $this->expectException(\LogicException::class);
    $this->expectExceptionMessage($expected_exception_message);
    $this->moduleHandler()->invoke('hook_single_invoke', 'custom_hook_invoke_string', args: [TRUE]);
  }

  /**
   * Tests invoke fails for multiple implementations that cannot be merged.
   */
  public function testInvokeWithNotMergeableClass(): void {
    $this->moduleInstaller()->install(['hook_single_invoke']);

    $expected_exception_message = 'Module hook_single_invoke should not implement custom_hook_invoke_class more than once.';
    $this->expectException(\LogicException::class);
    $this->expectExceptionMessage($expected_exception_message);
    $this->moduleHandler()->invoke('hook_single_invoke', 'custom_hook_invoke_class', args: [TRUE]);
  }

  /**
   * Returns the ModuleHandler.
   *