Verified Commit 3e6179a6 authored by godotislate's avatar godotislate
Browse files

feat: #3524377 Allow to skip OOP hooks and services for modules that are not installed

By: berdir
By: kristiaanvandeneynde
By: nicxvan
By: catch
By: acbramley
By: godotislate
parent 39c17921
Loading
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\Hook\Attribute;

/**
 * Makes hook registration dependent on a module being installed.
 *
 * If this attribute is added to a hook class or method, that class or method
 * will be skipped from registration when the module is not installed.
 *
 * Any module can be set; it does not need to be the module that invokes the
 * hook.
 *
 * Apart from performance benefits, when this attribute is applied to a class,
 * services provided by the module dependency can be injected as required
 * properties.
 *
 * @see \Drupal\node\Hook\NodeSearchHooks
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class HookDependsOnModule implements HookAttributeInterface {

  /**
   * Constructs HookDependsOnModule attribute object.
   *
   * @param string $module
   *   The name of the module that the hook depends on.
   */
  public function __construct(
    public readonly string $module,
  ) {

  }

}
+31 −1
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@
use Drupal\Core\Extension\ProceduralCall;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Hook\Attribute\HookAttributeInterface;
use Drupal\Core\Hook\Attribute\HookDependsOnModule;
use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Hook\Attribute\LegacyRequirementsHook;
@@ -373,6 +374,7 @@ protected function collectModuleHookImplementations($dir, $module, $current_modu
        if ($cached) {
          $class = $cached['class'];
          $attributes = $cached['attributes'];
          $class_attributes = $cached['class_attributes'] ?? [];
        }
        else {
          // Immediately after a deployment, opcache may not yet have refreshed
@@ -384,14 +386,42 @@ protected function collectModuleHookImplementations($dir, $module, $current_modu
          $namespace = preg_replace('#^src/#', "Drupal/$module/", $iterator->getSubPath());
          $class = $namespace . '/' . $fileinfo->getBasename('.php');
          $class = str_replace('/', '\\', $class);
          $class_attributes = [];
          $attributes = [];
          if (class_exists($class)) {
            $reflectionClass = new \ReflectionClass($class);
            $class_attributes = $reflectionClass->getAttributes(HookAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF);
            $class_attributes = array_map(static fn (\ReflectionAttribute $ra) => $ra->newInstance(), $class_attributes);
            $attributes = self::getAttributeInstances($reflectionClass);
            $hook_file_cache->set($filename, ['class' => $class, 'attributes' => $attributes]);
            $hook_file_cache->set($filename, [
              'class' => $class,
              'attributes' => $attributes,
              'class_attributes' => $class_attributes,
            ]);
          }
        }

        foreach ($class_attributes as $class_attribute) {
          // Skip the whole class if it depends on something that isn't
          // available. It will not be registered in the container then.
          if ($class_attribute instanceof HookDependsOnModule) {
            if (!in_array($class_attribute->module, $this->modules)) {
              continue 2;
            }
          }
        }

        foreach ($attributes as $method => $methodAttributes) {
          foreach ($methodAttributes as $attribute) {
            // Skip the method if it depends on something that isn't available.
            // If all methods are skipped, then the class will not be registered
            // in the container.
            if ($attribute instanceof HookDependsOnModule) {
              if (!in_array($attribute->module, $this->modules)) {
                continue 2;
              }
            }
          }
          foreach ($methodAttributes as $attribute) {
            if ($attribute instanceof Hook) {
              self::checkForProceduralOnlyHooks($attribute, $class);
+3 −1
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Hook\Attribute\HookDependsOnModule;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\views\FieldViewsDataProvider;
@@ -14,6 +15,7 @@
/**
 * Hook implementations for file.
 */
#[HookDependsOnModule('views')]
class FileViewsHooks {

  use StringTranslationTrait;
@@ -21,7 +23,7 @@ class FileViewsHooks {
  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly EntityFieldManagerInterface $entityFieldManager,
    protected readonly ?FieldViewsDataProvider $fieldViewsDataProvider,
    protected readonly FieldViewsDataProvider $fieldViewsDataProvider,
  ) {}

  /**
+2 −4
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

use Drupal\block\BlockInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Hook\Attribute\HookDependsOnModule;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Link;
use Drupal\menu_ui\MenuUiUtility;
@@ -366,11 +367,8 @@ public function formNodeTypeFormBuilder(string $entity_type, NodeTypeInterface $
   * Implements hook_ENTITY_TYPE_delete().
   */
  #[Hook('menu_delete')]
  #[HookDependsOnModule('node')]
  public function menuDelete(EntityInterface $entity): void {
    if (!$this->entityTypeManager->hasDefinition('node_type')) {
      return;
    }

    // Remove the menu from content type third party settings.
    $menu_id = $entity->id();
    $parent_prefix = $menu_id . ':';
+5 −0
Original line number Diff line number Diff line
name: 'Test Hook dependencies'
type: module
description: 'Test Hook classes and methods that depend on other modules'
package: Testing
version: VERSION
Loading