Loading core/lib/Drupal/Core/Hook/Attribute/HookDependsOnModule.php 0 → 100644 +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, ) { } } core/lib/Drupal/Core/Hook/HookCollectorPass.php +31 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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); Loading core/modules/file/src/Hook/FileViewsHooks.php +3 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -14,6 +15,7 @@ /** * Hook implementations for file. */ #[HookDependsOnModule('views')] class FileViewsHooks { use StringTranslationTrait; Loading @@ -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, ) {} /** Loading core/modules/menu_ui/src/Hook/MenuUiHooks.php +2 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 . ':'; Loading core/modules/system/tests/modules/hook_collector_depends_test/hook_collector_depends_test.info.yml 0 → 100644 +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
core/lib/Drupal/Core/Hook/Attribute/HookDependsOnModule.php 0 → 100644 +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, ) { } }
core/lib/Drupal/Core/Hook/HookCollectorPass.php +31 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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); Loading
core/modules/file/src/Hook/FileViewsHooks.php +3 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -14,6 +15,7 @@ /** * Hook implementations for file. */ #[HookDependsOnModule('views')] class FileViewsHooks { use StringTranslationTrait; Loading @@ -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, ) {} /** Loading
core/modules/menu_ui/src/Hook/MenuUiHooks.php +2 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 . ':'; Loading
core/modules/system/tests/modules/hook_collector_depends_test/hook_collector_depends_test.info.yml 0 → 100644 +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