Problem/Motivation

In #3485896: Hook ordering across OOP, procedural and with extra types i.e replace hook_module_implements_alter we introduced two properties to ModuleHandler:
- listenersByHook
- modulesByHook

These two arrays are lazily filled whenever a hook is called for the first time in a request/process.

The array keys in both arrays are aligned.
That is, isset($this->listenersByHook[$hook][$index]) if and only if isset($this->modulesByHook[$hook][$index]).

Currently it is up to ModuleHandler to maintain this alignment.
This works, but it puts a responsibility on a class which is already quite big.

Also, these are nested arrays, which are generally harder to reason about than single-dimensional arrays.

In #3506930: Separate hooks from events, we want to separate hooks from events, and as part of that we introduce an ImplementationList class. To reduce the complexity of that issue, we want to do this step separately, as a preparation.

Goals

Steps to reproduce

Proposed resolution

Introduce a new class ImplementationList, which is instantiated per hook, and which encapsulates the relationship between modules and implementations. Replace the two properties in ModuleHandler with a new property which is a map of ImplementationList objects.
Add a new protected method ModuleHandler::getHookImplementationList($hook).

Mark all of this `@internal`, to make it easier to change in follow-up issues.

Remove ModuleHandler::getHookListeners(), it is no longer needed.
(It is a protected method, I assume not considered public API).

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3519561

Command icon 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

nicxvan created an issue. See original summary.

nicxvan’s picture

Title: [pp-1] Explore adding an ImplementationList object for hook implmentations » Explore adding an ImplementationList object for hook implmentations

donquixote made their first commit to this issue’s fork.

donquixote’s picture

Issue summary: View changes
donquixote’s picture

Status: Active » Needs review

Let's have one round of review, to see if people agree with the direction or have different ideas where this should go.

andypost’s picture

donquixote’s picture

Issue summary: View changes

donquixote changed the visibility of the branch 3519561-DeprecationCanaryTest to hidden.

donquixote changed the visibility of the branch 3519561-hook-ImplementationList-remove-getHookListeners to hidden.

donquixote changed the visibility of the branch 3519561-DeprecationCanaryTest to hidden.

donquixote’s picture

I added the unit test for ImplementationList in this issue, to prepare for #3506930: Separate hooks from events.

nicxvan’s picture

Title: Explore adding an ImplementationList object for hook implmentations » Explore adding an ImplementationList object for hook implementations

Took a pass at it I think this is a pretty nice consolodation!

Quite a few comments and I definitely need to take a deeper look, but I like the direction.

nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

Ok I think this is ready!

I've read through this several times, both changes in isolation and the files as they will be merged.

This is a nice consolidation separating the concerns so module handler doesn't need to parse so much.
I checked through all of the places that ModuleHandler gets the hooks and confirmed they are replaced with ImplementationLists.

Early on I was concerned how this would work with Alter, but they are already ordered and they are retrieved before runtime ordering happens.

Tests look good, I didn't see anything missing a test.

nicxvan’s picture

Status: Reviewed & tested by the community » Needs work

Actually on second thought this might need a CR since we're adding a new object and we're changing some methods.

One comment on the MR.

donquixote’s picture

Actually on second thought this might need a CR since we're adding a new object and we're changing some methods.

Do we write change records for protected methods and protected properties?
And do we write change records for new classes marked as internal?

donquixote’s picture

Issue summary: View changes
nicxvan’s picture

Status: Needs work » Reviewed & tested by the community

Ok I asked in slack:

@godotislate shared this link https://www.drupal.org/about/core/policies/core-change-policies/bc-polic...

And @quietone shared this link https://www.drupal.org/about/core/policies/core-change-policies/change-r...

They basically said it depends on contrib usage and the docs say changes to internal code so not need a cr.

The thing is this isn't just a change to internal code it's renaming and changing the return type of a method.

I think it's low risk for a few reasons:
1 I maintain the only module I'm aware of that decorates module handler.
2 this is required to simplify the removal of event dispatcher from hooks and that will necessarily be a large bc break.
3 I don't see any way to cleanly deprecate getflathooklisteners.
4 the method itself is brand new.

I'm going to mark this as ready, I'm happy to write a cr if someone else thinks it's necessary.

nicxvan credited quietone.

nicxvan’s picture

Took a pass at credit, added @godotislate and @quietone for their assistance in slack.

donquixote’s picture

Title: Explore adding an ImplementationList object for hook implementations » Introduce ImplementationList objects per hook, to simplify ModuleHandler

Going for a simpler and more confident title.

nicxvan’s picture

Tagging this because it pseudo blocks #3506930: Separate hooks from events

nicxvan’s picture

Status: Reviewed & tested by the community » Needs work

Let's actually change the names from module / Modules to extensions so we can use this for themes too if we make them available.

I'm going to create a separate branch with that change.

nicxvan changed the visibility of the branch 11.x to hidden.

nicxvan’s picture

Status: Needs work » Needs review

Can we get a review of the new MR? All it is is the same MR @donquixote wrote but all references to module replaced with extension to make this more generic so we can use this with other extensions in the future.

godotislate’s picture

Status: Needs review » Needs work

Commented on MR 13020.

donquixote’s picture

Let's actually change the names from module / Modules to extensions so we can use this for themes too if we make them available.

I am not sure about this change.
The problems that we solve with this ImplementationList are (currently) very specific to module hooks.

For module hooks, every implementation needs a module associated with it, for these reasons:

  • In ModuleHandler->invokeAllWith(), the $module is passed to the callback.
  • In ModuleHandler->getCombinedListeners() (for ->alter()), the implementations are grouped by module before they are passed to hook_module_implements_alter().
  • In ModuleHandler->getCombinedListeners() (for ->alter()), when we apply the ordering rules, these also get access to the modules.

Also, until now, we don't have services registered for themes.

For theme hooks, we might be able work with a simpler ImplementationList which does not track the theme names.

On the other hand: If, in the future, we do align theme hooks more with module hooks, we may want to refactor parts of ModuleHandler and HookCollectorPass, moving shared functionality into a shared class.

Personally I would suggest to keep ImplementationList focused on modules for now, and also use the term "module" in variable names.
The class is already marked as internal, and the properties are protected, so it will be easy to change in the future.

In the future when we reuse this for themes we can either rename the properties, or we can create a new class, depending on the needs.

Until that happens, the code will be easier to understand if it says "$module" rather than "$extension".
We name things for what they are, not for what they could be.

godotislate’s picture

I touched on something similar in a MR comment, but I agree with #31. As proposed, the `extensions` property is a flat array AFAICT, and it would not be possible to differentiate between themes and modules in the future without refactoring. In which case, I think that suggests we should just use modules nomenclature for now and refactor whatever we need to accommodate themes later.

donquixote’s picture

I touched on something similar in a MR comment, but I agree with #31. As proposed, the `extensions` property is a flat array AFAICT, and it would not be possible to differentiate between themes and modules in the future without refactoring.

I imagine that even if we reuse this for theme hooks, we would have separate instances for modules and themes.
So we would never have a mix of modules and themes.
You would know from the context whether $list->extensions gives you only modules or only themes.

But, even if we want to reuse large parts of this ImplementationList class, we might still want to maintain two separate classes, even if they are similar, and even if the only difference is the name of these properties. DRY can be overrated.
I suspect there would be more differences, some methods might not be needed for themes.

nicxvan’s picture

Thank you for reviewing this, I have a couple of quick comments though:

Also, until now, we don't have services registered for themes.

OOP hooks in themes will necessarily include this.

For theme hooks, we might be able work with a simpler ImplementationList which does not track the theme names.

I don't think that is true, we have base themes and admin themes, so the same hook can before multiple themes and they all have to execute in the right order, but only for the active theme.

On the other hand: If, in the future, we do align theme hooks more with module hooks, we may want to refactor parts of ModuleHandler and HookCollectorPass, moving shared functionality into a shared class.

Yes, but that is not part of this MR at all.
Also ModuleHandler won't have much overlap if any at all and HookCollectorPass has less overlap than you might think.

The class is already marked as internal, and the properties are protected, so it will be easy to change in the future.

Just because it's internal doesn't mean we shouldn't try to plan ahead a bit.

There would be separate instances for themes and modules as @donquixote said in 33.

I think the theme version would only need: filterByExtensionNames

Maybe this isn't worth pursuing and we just have a much simplified ImplementationList for themes when the time comes.

nicxvan changed the visibility of the branch 3519561-ExtensionImplementationList to hidden.

nicxvan’s picture

I think you guys convinced me, there were a couple of suggestions from @godotislate that I think are relevant here, if we can apply those and confirm it's still green I think we can move this back to RTBC.

godotislate’s picture

Status: Needs work » Needs review

Rebased since HEAD's been fixed.

nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

Thanks! Back to rtbc the updates were fairly minor.

mstrelan’s picture

Status: Reviewed & tested by the community » Needs work

Left 3 suggestions. Was going to leave the status as they were quite subjective, but changing it because I believe list<string, string> is wrong.

See https://phpstan.org/r/6bf89217-0a74-48af-91fa-bddf512036f8

nicxvan’s picture

Status: Needs work » Needs review

Addressed all feedback, back to needs review!

nicxvan’s picture

One of the array_all updates broke the asserts so I undid them.
I don't think that @mstrelan considers that suggestion is enough to block this, but I'll leave that to @mstrelan to confirm, I made all other changes.

mstrelan’s picture

Yes it's not a blocker. I hadn't realised the callback for array_all requires both the value and the key. I think it would have to be written like this:

assert(array_all($listeners, fn ($value) => is_callable($value)));
assert(array_all($modules, fn ($value) => is_string($value)));
nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

Yeah, which i don't think it's much better let's leave it as is.

I'm going to go ahead and mark this ready since the only change was a phpstan type and test variable name.

needs-review-queue-bot’s picture

Status: Reviewed & tested by the community » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

nicxvan’s picture

Status: Needs work » Reviewed & tested by the community

Rebased, it was just the test conversion to attributes, nothing crucial changed, I'll keep an eye on tests.

berdir’s picture

I haven't caught up with the discussion here and even less so with #3506930: Separate hooks from events but I have some concerns about the container size from what I saw in the theme OOP issue, all the extra objects and services being serialized separtely aren't going to help. I'll try to do some tests on a real project with this.

nicxvan’s picture

Please let me know your findings, the event dispatcher is causing performance issues too though which is one of the reasons I'm still pursung this.

donquixote’s picture

I have some concerns about the container size from what I saw in the theme OOP issue, all the extra objects and services being serialized separtely aren't going to help.

Regarding theme hooks, yes, this would mean more services.

For the module hooks discussed in this issue, we already do have one service registered per class with hooks, and then some overhead in the event dispatcher. definition.

With this issue we don't get any changes to the container, because here the ImplementationList objects are created at runtime.

With the "separate hooks from events", we get one additional service per hook (if it has at least one implementation).
At the same time, we lose the additional overhead for the event dispatcher definition.
For the "container size" as in number and size of definitions, this could go either way.
For number and cost of instantiated services in an average request, I would hope that number to go down significantly.

I'll try to do some tests on a real project with this.

This would be nice to see!

berdir’s picture

Took quite a lot of time to update my distribution to 11.x, lots of patches to rebase. But yeah, I see, there's no meaningful difference between 11.x, in both cases the serialized string is around 1.3MB (which is... not great). I need to have a closer look at this and the other issue, I can see that the things I'm seeing are already being discussed there,

nicxvan’s picture

I'm not going to pull this from rtbc yet, but some additional discussion is happening here: https://www.drupal.org/project/drupal/issues/3506930

berdir’s picture

I agree with #50. I started testing my ideas a bit in the other issue and I understand a bit better how these currently work, in that they aren't really services but rely on having DI and get the resolved hook services injected. This would need to change with my approach so we might need to revert and change considerable aspects of this again.

donquixote’s picture

@berdir

I agree with #14.

Are you sure this is the comment number you want to reference? That comment does not look like something to agree or disagree with..
(I will delete this comment text after it has been clarified)

berdir’s picture

I have no idea how I came up with #14, I meant comment #50. edited my comment. You don't have to delete your comment, I can live with that mistake being out in the open ;)

nicxvan’s picture

Status: Reviewed & tested by the community » Postponed

I'm going to pull this - let's finish exploring that new direction. We can easily resurrect this if necessary.

nicxvan’s picture

Status: Postponed » Closed (outdated)
Issue tags: -11.3.0 release priority

Thanks everyone this is in directly from the parent!

Now that this issue is closed, please review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, please credit people who helped resolve this issue.