Commit f03b175f authored by catch's avatar catch
Browse files

Issue #3490355 by nicxvan, godotislate, catch: Add procedural hook short circuit per module or file

(cherry picked from commit 756c93fb)
parent 3cf29653
Loading
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\Hook\Attribute;

/**
 * Defines a StopProceduralHookScan attribute object.
 *
 * This allows contrib and core to mark when a file has no more
 * procedural hooks.
 */
#[\Attribute(\Attribute::TARGET_FUNCTION)]
class StopProceduralHookScan {

}
+71 −52
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@
use Drupal\Core\Extension\ProceduralCall;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

@@ -74,7 +75,7 @@ class HookCollectorPass implements CompilerPassInterface {
   * {@inheritdoc}
   */
  public function process(ContainerBuilder $container): void {
    $collector = static::collectAllHookImplementations($container->getParameter('container.modules'));
    $collector = static::collectAllHookImplementations($container->getParameter('container.modules'), $container);
    $map = [];
    $container->register(ProceduralCall::class, ProceduralCall::class)
      ->addArgument($collector->includes);
@@ -123,6 +124,8 @@ public function process(ContainerBuilder $container): void {
   * @param array $module_filenames
   *   An associative array. Keys are the module names, values are relevant
   *   info yml file path.
   * @param Symfony\Component\DependencyInjection\ContainerBuilder|null $container
   *   The container.
   *
   * @return static
   *   A HookCollectorPass instance holding all hook implementations and
@@ -130,15 +133,18 @@ public function process(ContainerBuilder $container): void {
   *
   * @internal
   *   This method is only used by ModuleHandler.
   *
   * * @todo Pass only $container when ModuleHandler->add is removed https://www.drupal.org/project/drupal/issues/3481778
   */
  public static function collectAllHookImplementations(array $module_filenames): static {
  public static function collectAllHookImplementations(array $module_filenames, ?ContainerBuilder $container = NULL): static {
    $modules = array_map(fn ($x) => preg_quote($x, '/'), array_keys($module_filenames));
    // Longer modules first.
    usort($modules, fn($a, $b) => strlen($b) - strlen($a));
    $module_preg = '/^(?<function>(?<module>' . implode('|', $modules) . ')_(?!preprocess_)(?!update_\d)(?<hook>[a-zA-Z0-9_\x80-\xff]+$))/';
    $collector = new static();
    foreach ($module_filenames as $module => $info) {
      $collector->collectModuleHookImplementations(dirname($info['pathname']), $module, $module_preg);
      $skip_procedural = isset($container) ? $container->hasParameter("$module.hooks_converted") : FALSE;
      $collector->collectModuleHookImplementations(dirname($info['pathname']), $module, $module_preg, $skip_procedural);
    }
    return $collector;
  }
@@ -153,13 +159,21 @@ public static function collectAllHookImplementations(array $module_filenames): s
   * @param $module_preg
   *   A regular expression matching every module, longer module names are
   *   matched first.
   * @param $skip_procedural
   *   Skip the procedural check for the current module.
   *
   * @return void
   */
  protected function collectModuleHookImplementations($dir, $module, $module_preg): void {
  protected function collectModuleHookImplementations($dir, $module, $module_preg, bool $skip_procedural): void {
    $hook_file_cache = FileCacheFactory::get('hook_implementations');
    $procedural_hook_file_cache = FileCacheFactory::get('procedural_hook_implementations:' . $module_preg);

    // Check only hook classes.
    if ($skip_procedural) {
      $dir = $dir . '/src/Hook';
    }

    if (is_dir($dir)) {
      $iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS);
      $iterator = new \RecursiveCallbackFilterIterator($iterator, static::filterIterator(...));
      $iterator = new \RecursiveIteratorIterator($iterator);
@@ -169,9 +183,10 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg)
        $extension = $fileinfo->getExtension();
        $filename = $fileinfo->getPathname();

      if (($extension === 'module' || $extension === 'profile') && !$iterator->getDepth()) {
        if (($extension === 'module' || $extension === 'profile') && !$iterator->getDepth() && !$skip_procedural) {
          // There is an expectation for all modules and profiles to be loaded.
          // .module and .profile files are not supposed to be in subdirectories.

          include_once $filename;
        }
        if ($extension === 'php') {
@@ -196,13 +211,16 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg)
            $this->addFromAttribute($attribute, $class, $module);
          }
        }
      else {
        elseif (!$skip_procedural) {
          $implementations = $procedural_hook_file_cache->get($filename);
          if ($implementations === NULL) {
            $finder = MockFileFinder::create($filename);
            $parser = new StaticReflectionParser('', $finder);
            $implementations = [];
            foreach ($parser->getMethodAttributes() as $function => $attributes) {
              if (StaticReflectionParser::hasAttribute($attributes, StopProceduralHookScan::class)) {
                break;
              }
              if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches)) {
                $implementations[] = ['function' => $function, 'module' => $matches['module'], 'hook' => $matches['hook']];
              }
@@ -221,6 +239,7 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg)
        }
      }
    }
  }

  /**
   * Filter iterator callback. Allows include files and .php files in src/Hook.
+5 −0
Original line number Diff line number Diff line
name: 'Test skipping procedural for whole module'
type: module
description: 'Test skipping procedural for whole module.'
package: Testing
version: VERSION
+18 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Implement hooks.
 */

declare(strict_types=1);

/**
 * This implements a hook but should not be picked up.
 *
 * We have set procedural_hooks: skip.
 */
function hook_collector_skip_procedural_cache_flush(): void {
  // Set a global value we can check in test code.
  $GLOBALS['skip_procedural_all'] = 'skip_procedural_all';
}
+2 −0
Original line number Diff line number Diff line
parameters:
  hook_collector_skip_procedural.hooks_converted: true
Loading