Closed (fixed)
Project:
Drupal core
Version:
11.x-dev
Component:
node system
Priority:
Normal
Category:
Task
Assigned:
Unassigned
Reporter:
Created:
27 Aug 2025 at 16:04 UTC
Updated:
9 Oct 2025 at 14:54 UTC
Jump to comment: Most recent
#3540386-10: [meta] Explore using more service closures to break deep dependency chains and load fewer services identifies the form builder service passed into node module Hook services as something that could be passed as a service closure instead.
Searching through the code, it looks like the only affected class is NodeTheme Hooks.
Use a service closure in NodeThemeHooks for the form builder property.
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 #3
godotislateComment #4
nicxvan commentedLooks right to me!
So if I understand it correctly this means it will only be set up once it's used by the page_top. Another way to do that would be to move the page_top implementation to it's own class right?
Comment #5
godotislateAssuming a Hook class is instantiated only at the time when any one of its methods invoked, then probably yes.
Comment #6
berdirOn standard install profile, this doesn't make a difference yet, because it's already initialized by \Drupal\search\Plugin\Block\SearchBlock.
Lets do a separate issue for that, because we do initialize plugins even when they are render cached.
Comment #7
godotislateIn that issue, are we looking for a way to load services lazily in plugin classes? Or prevent block plugins from being instantiated if they're render cached? Or something else specific to the search block?
Comment #8
berdirValid question. 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:
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.
Comment #9
mstrelan commentedThe downside to all these closures is we lose type safety. If we use Lazy Services instead we don't have that issue, but I suspect there's a reason we've already landed on service closures.
Moving it to its own class could be worthwhile. We're injecting RendererInterface and EntityTypeManagerInterface to hook_page_top even though we don't need them. In saying that, they're probably going to be loaded anyway.
Comment #10
berdirSee #3396928: Deprecate Drupal ProxyBuilder in favor of Symfony lazy services on lazy services.
But yes, missing types is unfortunate. By allowing no docs on constructors. we also lose the ability to declare it for phpstan too with a docblock. We could say that closures can't be promoted properties but use a protected \Closure $fooClosure with an appropriate docblock? Or we add a protected getFormBuilder() method that returns the result of the closure?
Comment #11
godotislateCreated #3544405: [PP-1] Replace SearchBlock service properties with service closures per #8.
Also related: #3514491: [meta] Replace lazy service proxy generation with service closures
Comment #12
mstrelan commentedIt seems we can do this:
PhpStorm resolves the type and phpcs doesn't complain. Presumably phpstan would understand this too. It's not very pretty though.
Comment #13
mstrelan commentedOne more thing to consider, in Drupal 12 (PHP 8.4) we can do this:
And then just call the property directly rather than the closure.
Comment #14
godotislateI like #13. Maybe we should add a 'Major version only' follow up issue?
Comment #15
catchYeah #13 looks really good, a new issue makes sense, maybe a meta for using property hooks with service closures or something like that?
Comment #16
berdirThe syntax in #13 is neat, FWIW, the following is almost identical in terms of complexity IMHO and we could use that now already:
Only thing is that it might need more docs than a property?
Comment #17
catchWe have #3376518: Allow omitting @var, @param and @return tags or their descriptions if the variable name and type is enough which would mean no extraneous docs for the method in this case, but it's not closed yet.
Comment #18
godotislateWhat about using an assert() for now and then doing property hooks later?
Something like this:
Comment #19
catchThat works for me. It will be a bit annoying if the same closure is used in multiple different hook methods, but we can fix that in 12.x with property hooks anyway.
Comment #20
godotislateStub meta issue for property hooks: #3544903: [meta] Use property hooks to instantiate services from service closures
Comment #21
godotislateSending back to NR for commit per #18.
Comment #22
acbramley commentedCan't wait for those property hooks, this is looking good now
Comment #23
acbramley commentedFYI we also have #3339905: Add page_top and page_bottom to #attached to get rid of NodeThemeHooks::pageTop() which would make this moot.
Comment #24
catchCommitted/pushed to 11.x, thanks!
Comment #28
eric_a commentedWhy did we switch from a normal typed property to a local variable? As it stands, if we move to a virtual property we would have to move back from local variable to property.
If we had kept the typed $formBuilder property then:
- the
assert()would not be needed, right?- we would not have to bring back the property on PHP 8.4. (Even if not readonly anymore.)
- the rest of the code would not have had to switch to the local variable, right? (And back later.)
(EDITED)
Comment #29
eric_a commentedSo maybe the reason is that PHPStan complains when a readonly property is assigned outside of the constructor. One solution would be to just remove the
readonlyalready, even if it looks a bit odd. Makes it even easier to transition to a virtual property.