Commit 366cef4e authored by dpi's avatar dpi
Browse files

Issue #3266008 by dpi, larowlan: Add a compiler pass to auto-discover services

parent 3f4cbc36
Loading
Loading
Loading
Loading
+102 −6
Original line number Diff line number Diff line
@@ -4,35 +4,131 @@ declare(strict_types=1);

namespace Drupal\hux;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\hux\Attribute\Alter;
use Drupal\hux\Attribute\Hook;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;

/**
 * Hux compiler pass.
 *
 * Drupals' service_collector via TaggedHandlersPass requires the 'call' method
 * to implement an interface. We don't require Hook implementors to implement an
 * interface.
 * Find files in src/Hooks directories in modules and adds them to the
 * container as a service with a 'hooks' tag.
 *
 * Adds services tagged with 'hooks' as a method call to the Hux module handler.
 * Drupals' service_collector cannot be used since TaggedHandlersPass requires
 * the 'call' method to implement an interface: We don't require Hook
 * implementors to implement an interface.
 */
final class HuxCompilerPass implements CompilerPassInterface {

  /**
   * {@inheritdoc}
   */
  public function process(ContainerBuilder $container) {
    $definition = $container->findDefinition('hux.module_handler');
  public function process(ContainerBuilder $container): void {
    /** @var class-string[] $hooksClasses */
    $hooksClasses = [];
    foreach ($container->findTaggedServiceIds('hooks') as $id => $tags) {
      $hooksClasses[] = $container->getDefinition($id)->getClass();
    }

    foreach ($this->getHuxClasses($container->getParameter('container.namespaces')) as $className) {
      // Don't create a service definition if this class is already a service.
      if (in_array($className, $hooksClasses, TRUE)) {
        continue;
      }

      $idSuffix = (new CamelCaseToSnakeCaseNameConverter())
        ->normalize(str_replace('\\', '_', $className));
      $definition = new Definition($className);
      $definition
        ->addTag('hooks')
        ->setPrivate(TRUE);

      if ((new \ReflectionClass($className))->isSubclassOf(ContainerInjectionInterface::class)) {
        $definition
          ->setFactory([$className, 'create'])
          ->setArguments([new Reference('service_container')]);
      }

      $container->setDefinition('hux.auto.' . $idSuffix, $definition);
    }

    $huxModuleHandler = $container->findDefinition('hux.module_handler');

    foreach ($container->findTaggedServiceIds('hooks') as $id => $tags) {
      $serviceDefinition = $container->getDefinition($id);
      /** @var class-string|null $className */
      $className = $serviceDefinition->getClass();
      preg_match_all('/^Drupal\\\\(?<moduleName>[a-z_0-9]{1,32})\\\\.*$/m', $className, $matches, PREG_SET_ORDER);
      $moduleName = $matches[0]['moduleName'] ?? throw new \Exception(sprintf('Could not determine module name from class %s', $className));

      $definition->addMethodCall('addHookImplementation', [
      $huxModuleHandler->addMethodCall('addHookImplementation', [
        $id,
        $moduleName,
      ]);
    }
  }

  /**
   * Get Hux classes for the provided namespaces.
   *
   * @param array<class-string, string> $namespaces
   *   An array of namespaces. Where keys are class strings and values are
   *   paths.
   *
   * @return \Generator<class-string>
   *   Generates class strings.
   *
   * @throws \ReflectionException
   */
  private function getHuxClasses(array $namespaces) {
    foreach ($namespaces as $namespace => $dirs) {
      $dirs = (array) $dirs;
      foreach ($dirs as $dir) {
        $dir .= '/Hooks';
        if (!file_exists($dir)) {
          continue;
        }
        $namespace .= '\\Hooks';

        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST);
        foreach ($iterator as $fileinfo) {
          if ($fileinfo->getExtension() !== 'php') {
            continue;
          }

          /** @var \RecursiveDirectoryIterator|null $subDir */
          $subDir = $iterator->getSubIterator();
          if (NULL === $subDir) {
            continue;
          }

          $subDir = $subDir->getSubPath();
          $subDir = $subDir ? str_replace(DIRECTORY_SEPARATOR, '\\', $subDir) . '\\' : '';
          $class = $namespace . '\\' . $subDir . $fileinfo->getBasename('.php');

          $reflectionClass = new \ReflectionClass($class);

          $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC);
          foreach ($methods as $reflectionMethod) {
            if (count($reflectionMethod->getAttributes(Hook::class)) > 0) {
              yield $class;
              break;
            }

            if (count($reflectionMethod->getAttributes(Alter::class)) > 0) {
              yield $class;
              break;
            }
          }
        }
      }
    }
  }

}
+4 −0
Original line number Diff line number Diff line
name: Hux Auto Test
type: module
description: Tests for HUX.
package: Testing
+40 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\hux_auto_test\Hooks;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\hux\Attribute\Hook;
use Drupal\hux_test\HuxTestCallTracker;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * A hooks class with container injection.
 */
final class HuxAutoContainerInjection implements ContainerInjectionInterface {

  /**
   * Creates a new HuxAutoContainerInjection.
   */
  public function __construct(
    protected TimeInterface $time,
  ) {
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('datetime.time'),
    );
  }

  #[Hook('test_hook')]
  public function testHook(string $something): void {
    HuxTestCallTracker::record([__CLASS__, __FUNCTION__, $something, $this->time->getRequestTime()]);
  }

}
+12 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\hux_auto_test\Hooks;

/**
 * A hooks class with no hooks.
 */
final class HuxAutoEmpty {

}
+25 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\hux_auto_test\Hooks;

use Drupal\hux\Attribute\Hook;
use Drupal\hux_test\HuxTestCallTracker;

/**
 * A hooks class with multiple hooks.
 */
final class HuxAutoMultiple {

  #[Hook('test_hook')]
  public function testHook1(string $something): void {
    HuxTestCallTracker::record([__CLASS__, __FUNCTION__, $something]);
  }

  #[Hook('test_hook')]
  public function testHook2(string $something): void {
    HuxTestCallTracker::record([__CLASS__, __FUNCTION__, $something]);
  }

}
Loading