Replace SearchBlock service properties with service closures

Created on 2 September 2025, 2 months ago

Problem/Motivation

While replacing the form builder service in NodeThemeeHooks with a service closure in 📌 Use service closure for form builder in node module hook classes Active , @berdir noted that on Standard install, the form builder still gets instantiated on every request because the search block is instantiated:

On standard install profile, this doesn't make a difference yet, because it's already initialized by \Drupal\search\Plugin\Block\SearchBlock.

This issue is to address how to inject the form builder and search page repository services lazily via service closures into the SearchBlock plugin class.

Steps to reproduce

Per #3543386-8: Use service closure for form builder in node module hook classes :

We'd want to do the same as we do here. we can't not instantiate block plugins.

plugins self-inject, so we need to define our own closure, ugly, but not not rocket science. Based on some unit tests I had to update, this should work:

ublic function __construct(array $configuration, $plugin_id, $plugin_definition, \Closure $form_builder, \Closure $search_page_repository) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->formBuilder = $form_builder;
    $this->searchPageRepository = $search_page_repository;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static($configuration, $plugin_id, $plugin_definition,
      fn() => $container->get('form_builder'),
      fn() => $container->get('search.search_page_repository')
    );
  }

So we just define the closure by hand.

Definitely a new issue, because for existing code, we have to think about BC. While working on 📌 Lazy inject database into session manager Active , I was thinking we could do something like \Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait, but for closures by following a standard naming pattern.

So we'd change protected FormBuilderInterface $formBuilder to protected \Closure $formBuilderClosure. And then the trait, if a subclasses access $this->formBuilder which no longer exists, just checks with a magic __get() if $this->{$name}Closure exists, is a closure and calls it. But we can discuss that further in that or a separate new issue.

Proposed resolution

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

📌 Task
Status

Active

Version

11.0 🔥

Component

plugin system

Created by

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

Merge Requests

Comments & Activities

  • Issue created by @godotislate
  • Pipeline finished with Success
    2 months ago
    Total: 408s
    #588246
  • Put up a draft MR to see if tests pass, and they do.

    Tagging for Needs CR for the constructor argument deprecations, will add that later. I know there was discussion in the original issue about typing, so we can bring that over here. For now, I added a return type declaration to the closures.

  • One thought that doesn't apply to SearchBlock, but if we continue to use closures for services like this in plugins (and similar), will probably need to be careful with any classes that can be serialized and make sure to override __sleep() and __wakeup().

  • Pipeline finished with Success
    2 months ago
    Total: 1274s
    #588285
  • 🇨🇭Switzerland berdir Switzerland

    > One thought that doesn't apply to SearchBlock

    Good thought and I'm not even sure it doesn't apply to that. There's a reason that \Drupal\Core\DependencyInjection\DependencySerializationTrait is used all over the place. I think it would be fairly easy to extend that trait and support closures? It's just an extra elseif to check if something is a closure and it's return value is a service? do we need reflection to only call closures without arguments?

  • Good thought and I'm not even sure it doesn't apply to that. There's a reason that \Drupal\Core\DependencyInjection\DependencySerializationTrait is used all over the place. I think it would be fairly easy to extend that trait and support closures?

    I missed that PluginBase does use DependencySerializationTrait, so SearchBlock could be serialized.

    As for serialization/unserialization, I think you're right. It could look something like this (untested):

      protected $_serviceClosureIds = [];
    
      public function __sleep(): array {
        $vars = get_object_vars($this);
        try {
          $container = \Drupal::getContainer();
          $reverse_container = $container->get(ReverseContainer::class);
          foreach ($vars as $key => $value) {
            if (!is_object($value) || $value instanceof TranslatableMarkup) {
              // Ignore properties that cannot be services.
              continue;
            }
            if ($value instanceof EntityStorageInterface) {
              // If a class member is an entity storage, only store the entity type
              // ID the storage is for, so it can be used to get a fresh object on
              // unserialization. By doing this we prevent possible memory leaks
              // when the storage is serialized and it contains a static cache of
              // entity objects. Additionally we ensure that we'll not have multiple
              // storage objects for the same entity type and therefore prevent
              // returning different references for the same entity.
              $this->_entityStorages[$key] = $value->getEntityTypeId();
              unset($vars[$key]);
            }
            elseif ($value instanceof \Closure && ($service_id = $reverse_container->getId(($value)()))) {
              $this->_serviceClosureIds[$key] = $service_id;
              unset($vars[$key]);
            }
            elseif ($service_id = $reverse_container->getId($value)) {
              // If a class member was instantiated by the dependency injection
              // container, only store its ID so it can be used to get a fresh
              // object on unserialization.
              $this->_serviceIds[$key] = $service_id;
              unset($vars[$key]);
            }
          }
        }
        catch (ContainerNotInitializedException) {
          // No container, no problem.
        }
    
        return array_keys($vars);
      }
    
      public function __wakeup(): void {
        // Avoid trying to wakeup if there's nothing to do.
        if (empty($this->_serviceIds) && empty($this->_entityStorages)) {
          return;
        }
        $container = \Drupal::getContainer();
        foreach ($this->_serviceIds as $key => $service_id) {
          $this->$key = $container->get($service_id);
        }
        $this->_serviceIds = [];
    
        foreach ($this->_serviceClosureIds as $key => $service_closure_id) {
          $this->$key = static fn() => $container->get($service_closure_id);
        }
    
        if ($this->_entityStorages) {
          /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
          $entity_type_manager = $container->get('entity_type.manager');
          foreach ($this->_entityStorages as $key => $entity_type_id) {
            $this->$key = $entity_type_manager->getStorage($entity_type_id);
          }
        }
        $this->_entityStorages = [];
      }
    

    Should that be done here? Or another issue?

  • 🇨🇭Switzerland berdir Switzerland

    Lets do another issue? We'll likely need to add a separate test for it anyway.

  • do we need reflection to only call closures without arguments?

    Interesting. It seems possible that a class could have a property whose value is a closure that takes at least 1 argument, so trying to call that closure without arguments during serialization would cause an error. I don't see a way to check for that other than using relfection on the closure.

  • Pipeline finished with Success
    about 2 months ago
    #601381
  • Pipeline finished with Success
    17 days ago
    Total: 668s
    #636381
  • Pipeline finished with Success
    15 days ago
    Total: 453s
    #638731
  • Pipeline finished with Success
    15 days ago
    Total: 490s
    #638734
Production build 0.71.5 2024