Problem/Motivation
While replacing the form builder service in NodeThemeeHooks with a service closure in #3543386: Use service closure for form builder in node module hook classes, @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:
public 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 #3541705: Lazy inject database into varous services, 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
Issue fork drupal-3544405
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
godotislateComment #4
godotislatePut 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.
Comment #5
godotislateOne 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().Comment #6
berdir> 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?
Comment #7
godotislateI 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):
Should that be done here? Or another issue?
Comment #8
berdirLets do another issue? We'll likely need to add a separate test for it anyway.
Comment #9
godotislateInteresting. 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.
Comment #10
godotislateIssue to handle serialization: #3544994: Make service closures serializable in classes using DependencySerializationTrait
Comment #11
godotislateChecked some other existing deprecation messages for constructor parameters, and some of them link to the issue and not a change record, so I decided just to do that. Removing "Needs change record" tag and moving MR out of draft.
Comment #12
smustgrave commentedLeft a small comment but leaving in review for others.
Comment #13
dcam commentedThis plugin is one of those having its
create()function removed by #3552110: Remove manual create() method from plugins in favor of autowiring. How do these issues affect each other?Is it possible to add the
#[AutowireServiceClosure(...)]attribute and deletecreate()here? In which case I'd remove it from #3552110.Or do the parameter deprecations and need to handle either the closures or original parameters negate the possibility of using autowiring?
Also, does the CR need before and after examples?
Comment #14
godotislateThere needs to be a separate issue to support that. It was on my todo list to create one, but I haven't gotten around to it yet and thought it would be best to land this and #3544994: Make service closures serializable in classes using DependencySerializationTrait first.
For the class's constructor arguments deprecation? I'm not sure how that would go.
Comment #15
dcam commentedOk, maybe I should go ahead and drop it from #3552110: Remove manual create() method from plugins so we don't have an inevitable merge conflict.
Yeah, IDK either. Who constructs instances of plugins manually? But I've had to provide examples for such things in CRs like:
Before:
After:
You can use that if someone insists on it being in the CR.
Comment #16
dcam commentedOh, the questions I had while reviewing were answered. So I'll go ahead and RTBC this. The code looks OK to me. This service closure pattern has me thinking about where it can be applied elsewhere.
Comment #17
berdirI'm less concerned about the constructor and more about the properties which change type and behavior. I think there is an option on how we could make this work, the question is if we care enough (short: renamed properties + __get()). It's unlikely that someone has customized the constructor and we BC for that, but if someone would access the properties it would be a fatal error now.
Comment #18
dcam commented@berdir I see your point. So would it be sufficient to do something like this?
Comment #19
berdirYes, but we need to give the new properties a new name, for example formBuilderClosure, or __get() won't be triggered. If we'd adopt that naming convention, it could even be generic, similar to \Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait.
Comment #20
godotislateAdded the BC for the class properties. I genericized the
__get(), but then didn't do a trait because the properties aren't really deprecated, since we'll be bringing them back via property hooks in D12 with minimum PHP 8.4 support in #3544903: [meta] Use property hooks to instantiate services from service closures. So the__get()will only be a thing for this short window of time.Comment #21
godotislateStubbed the follow up #3554337: [PP-1] Support the AutowireServiceClosure attribute in AutowireTrait to inject service closures per #14.
Comment #22
berdirThanks. I'm not sure how often we want to apply this pattern , at least not before the two related issues of which one is D12+ to deal with extra code and DX. But In this specific case, it results in a measurable improvement (maybe not in terms of actual time savings but in number of services instantiated) in a common scenario.
Added one optional suggestion to the MR, but I think this can go back to RTBC.
I think search module is a better component here as it's not (yet) a generic plugin system thing.
Comment #23
godotislateThanks for the reviews.
I wondering now if this should be postponed on #3544994: Make service closures serializable in classes using DependencySerializationTrait?
Comment #24
godotislateBlocking on #3544994: Make service closures serializable in classes using DependencySerializationTrait.