Prevent preprocess hooks from being implemented

Created on 7 February 2022, over 2 years ago
Updated 9 April 2024, 5 months ago

As invoked by ThemeManager.

📌 Task
Status

Active

Version

1.0

Component

Code

Created by

🇦🇺Australia dpi Perth, Australia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇩🇪Germany donquixote

    Could you implement hook_preprocess in hux itself (as in hux_preprocess) and then have that call the manager?

    There is a nicer way to implement preprocess:
    Use hook_theme_registry_alter() to register the preprocess callbacks.

    Unfortunately as it is now these functions cannot be methods on services.
    From ThemeManager::render():

          foreach ($info['preprocess functions'] as $preprocessor_function) {
            if (is_callable($preprocessor_function)) {
              call_user_func_array($preprocessor_function, [&$variables, $hook, $info]);
            }
          }
    

    But we could use tricks with __callStatic().
    https://3v4l.org/SlUU8
    https://3v4l.org/kPUuQ
    https://3v4l.org/JBk1u

    
    class MyService {
        function preprocess(array &$variables): void {
            $variables['x'] = 'X';
        }
    }
    
    class Drupal {
        static function service($id): object {
            return new MyService();
        }
    }
    
    class ServiceCallback {
        static function __callStatic(string $f, array $args) {
            ['service' => $service_id, 'method' => $method] = unserialize($f);
            $service = \Drupal::service($service_id);
            $service->$method(...$args);
        }
    }
    
    $variables = [];
    
    $callback = ['ServiceCallback', serialize(['service' => 'myservice', 'method' => 'preprocess'])];
    call_user_func_array($callback, [&$variables]);
    
    assert($variables === ['x' => 'X']);
    
  • 🇩🇪Germany donquixote

    The following already works with current hux:

    #[\Attribute(\Attribute::TARGET_METHOD)]
    class Preprocess {
    
      /**
       * Constructor.
       *
       * @param string $hook
       *   Name of the theme hook.
       */
      public function __construct(
        public readonly string $hook,
      ) {}
    
    }
    
    /**
     * Base class for classes with theme preprocess callbacks.
     */
    abstract class PreprocessBase {
    
      /**
       * Constructor.
       *
       * @param \Drupal\Component\DependencyInjection\ReverseContainer $reverseContainer
       */
      public function __construct(
        private readonly ReverseContainer $reverseContainer,
      ) {}
    
      /**
       * Implements hook_theme_registry_alter().
       *
       * @param array<string, array> $registry
       *   Theme registry.
       */
      #[Alter('theme_registry')]
      public function themeRegistryAlter(array &$registry): void {
        $reflection_class = new \ReflectionClass(static::class);
        $methods = $reflection_class->getMethods(\ReflectionMethod::IS_PUBLIC);
        foreach ($methods as $method) {
          $attributes = $method->getAttributes(Preprocess::class);
          if ($attributes === []) {
            continue;
          }
          if ($method->isStatic()) {
            $preprocess_callback = [static::class, $method->getName()];
          }
          else {
            $service_id = $this->reverseContainer->getId($this);
            assert($service_id !== NULL);
            $preprocess_callback = [static::class, $service_id . '->' . $method->getName()];
          }
          foreach ($attributes as $attribute) {
            $attribute_object = $attribute->newInstance();
            assert($attribute_object instanceof Preprocess);
            $hook = $attribute_object->hook;
            if (!isset($registry[$hook])) {
              continue;
            }
            $registry[$hook]['preprocess functions'][] = $preprocess_callback;
          }
        }
      }
    
      /**
       * Invokes a preprocess method on a service object.
       *
       * @param string $name
       *   Synthesized method name, contains the service id and actual method name.
       * @param array $arguments
       *   Arguments to pass to the actual method.
       */
      public static function __callStatic(string $name, array $arguments): void {
        [$service_id, $method_name] = explode('->', $name);
        $service = \Drupal::service($service_id);
        call_user_func_array([$service, $method_name], $arguments);
      }
    
    }
    
    class MyPreprocessHooks extends PreprocessBase {
    
      /**
       * Theme preprocess for 'block'.
       */
      #[Preprocess('block')]
      public function preprocessBlock(array &$variables): void {...}
    
    }
    

    For a proper solution we would not rely on ReverseContainer and inheritance, instead this would work out of the box for all classes with hooks.

  • 🇦🇺Australia dpi Perth, Australia

    After two large unsuccessful attempts at working with the theme system, its absolutely not something I want to bring into Hux proper.

    Something needs to happen with core, as ThemeManager and friends are hell to work with in 2024.

    Is something above worth extracting to its own project? Is it reliable/predictable?

    Side note, if what you're dealing with are your own theme implementations, not of core or others, you might want to check out Pinto .

  • 🇩🇪Germany donquixote

    Something needs to happen with core, as ThemeManager and friends are hell to work with in 2024.

    The ThemeManager is a beast indeed, especially the global state around the "current theme".

    But adding preprocess callbacks via hook_theme_registry_alter() is quite transparent.
    I have done this a lot in Drupal 7, and the mechanism still is the same.

    Is something above worth extracting to its own project? Is it reliable/predictable?

    Yes, it seems this could go into a separate project.
    It could even be fully independent of Hux, although it would be nice to reuse the discovery mechanism.

    It seems quite reliable so far.
    One thing I don't like is the `\Drupal::service()` calls.
    Perhaps theme preprocess will be changed to support service methods, similar to '#pre_render'.

    One tricky part could be the more specialized theme hooks, like `region__content`. I will have to investigate a bit more what happens if we add a `$registry['region__content']['preprocess functions'][] = ...`, if there was not such a hook originally.

    I like to put multiple hook implementations (and preprocess) into the same class if all belong to the same "behavior".
    So I think I would activate the `Hooks` namespace for preprocess.

Production build 0.71.5 2024