Problem/Motivation
In #3442009: OOP hooks using attributes and event dispatcher we added NodeHooks.php with a couple of sample hooks. When the remaining procedural hooks were converted in #3483599: Convert all procedural hook implementations to Hook classes they were placed in a separate file NodeHooks1.php to avoid conflicts. Since there is no logical split, these should be combined. Later on these could be split in a logical way, e.g. by dependencies or by the module that invokes the hooks.
Steps to reproduce
$ ls -1 core/modules/node/src/Hook/NodeHooks*
core/modules/node/src/Hook/NodeHooks1.php
core/modules/node/src/Hook/NodeHooks.php
Proposed resolution
Split hooks by api.php see #29
Remaining tasks
Review
API changes
NodeHooks have been split by api
| Comment | File | Size | Author |
|---|
Issue fork drupal-3511357
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
catchIt's not a 100% duplicate, e.g. query_node_access_alter only appears to be in this file. Might be necessary to merge it with NodeHooks.
Comment #3
charlliequadros commentedHi @catch
I'm not sure if we should remove the file, because from what I understand in Drupal 11, when using notation to call hooks, it's a good practice to separate the hooks into different files for better organization.
Maybe, instead of deleting the file, it would be better to just rename it. If I'm wrong, could someone help me and provide advice on the best way to handle this?
NodeHooks
NodeHooks1
Comment #4
mstrelan commentedI don't think it's a duplicate at all? The original
NodeHooks.phpwas added in #3442009: OOP hooks using attributes and event dispatcher, I believe as a demonstration of implementing the same hook twice (hook_user_cancel). The newerNodeHooks1.phpwas added in #3483599: Convert all procedural hook implementations to Hook classes when all the remaining hooks were converted. I think we should move the 2 hooks in NodeHooks to NodeHooks1, delete NodeHooks and then rename NodeHooks1 to NodeHooks. Any further refactoring should be done later.Comment #6
mstrelan commentedSorry, didn't see the Novice tag. Should have left it.
Comment #7
catchGiven it's not a duplicate it's also not a novice issue.
Comment #9
smustgrave commentedSo re-ran a few times but some performance tests are consistently failing
Comment #10
mstrelan commentedDrupal\Tests\navigation\FunctionalJavascript\PerformanceTest::testLogin
I put a breakpoint in
\Drupal\Tests\PerformanceTestTrait::collectPerformanceDataand compared$performance_test_data['cache_operations']between HEAD and this issue. These are the 2 additional cache gets:Drupal\Tests\standard\FunctionalJavascript\StandardPerformanceTest::testStandardPerformance
The diff in the array comparison is:
So it looks like both tests are failing for the same reasons, i.e.
entity_type_definitions.installedandnode.field_storage_definitions.installed.I would imagine this is because of dependency injection. Previously we were injecting the entity type manager and loading node storage in NodeHooks.php, but this was only ever invoked for
hook_user_cancel. Now that this is combined with NodeHooks1 we're injecting these for far more hooks. I think in this case we can simply update the performance test expectations, but this also highlights why it might be worth splitting hooks classes based on what dependencies are required. Alternatively, we could revert to usingDrupal::servicehere to reduce the number of cache gets here.Also was running in to some other issues but they seem to be fixed in #3511123: Remove cache tag checksum assertions from performance tests, so I merged in 11.x again.
Comment #11
smustgrave commentedUpdate to performance tests appear to fix everything.
Comment #13
acbramley commentedBased on the description in #10 I don't know if I agree with combining these all into 1 class.
We now have 21 hooks in a single class, 19 of which don't need the injected services (all from NodeHooks1).
IMO we could just rename NodeHooks1 to something a bit more descriptive? We don't have to shove every single hook into a single class.
Comment #14
acbramley commentedNeed an IS update too as it's not a duplicate file.
Comment #15
acbramley commentedOne idea I had was whether we could make all hooks that don't use services into
staticfunctions, and then we could group those hooks intoNodeStaticHooks. I've tested this and it does seem to work, however I don't know if there's any performance benefit from making them static, but it seems like it might?Comment #16
quietone commentedClosed #3489095: Rename NodeHooks1 to something meaningful as a duplicate.
Comment #17
catchYes #10 makes this tricky, we should at least make sure the extra service instantiations aren't in the critical path.
Comment #18
nicxvan commentedJust a quick note, this only exists because of a file exists when running rector the script creates a new file and increments. When I ran core we had only created the one core conversion in NodeHooks so NodeHooks1 was created.
There is no duplication it is just manual vs automated conversions.
Comment #19
mstrelan commentedThanks @nicxvan, that's the same conclusion I came to in #4. I've updated the IS to clarify.
I've pushed another branch to see how the tests look if we remove dependency injection in this class. There are already plenty of other services in the class accessed by
\Drupal::*.Comment #21
nicxvan commentedNo need to do this in two steps, I think we're leaning towards naming the file based on the api where the hook is defined.
Then we can add DI for each.
If there is a problematic one we can split further. I've added the meta.
Comment #23
mstrelan commentedOK that's good to know. I had another idea that we could use service closures and the
#[AutowireServiceClosure]attribute in hook classes. I replaced every\Drupal::call in NodeHooks with a service closure and re-ran the performance tests. This resulted in the same numbers as when using the static everywhere. Then I switched to directly injecting all the services and ran the tests again, expecting a worse score, but again it was the same as using static everywhere. Not sure what that tells us, injecting some services results in a performance regression but injecting all services does not. I couldn't get all the tests passing though, so maybe I've broken something.Comment #24
nicxvan commentedSorry, we do need to split the file though, we don't want to be injecting everything on every hook.
Comment #25
mstrelan commentedTotally agree. The point of that MR was to compare performance. For some reason MR !11406 requires adjustments to existing performance tests when injecting two services, but that same test passes in MR !11511 without adjustments when injecting all services. So I'm trying to figure out why there is a discrepancy.
Comment #27
mstrelan commentedI figured out why there was a performance regression in some branches but not others. In the first MR I had kept the node storage load in the constructor, even after I commented that it was an anti-pattern. But in the additional branches I had abstracted that out and only loaded when needed.
I've updated the original MR to not load the node storage in the constructor and reverted the expected performance test metrics to the original values.
Going to leave it there to decide if we want to proceed with this or split things further as per #21.
Comment #28
nicxvan commentedGlad you sorted that out!
I think my general recommendation is going to be a 3 part solution for all hooks.
I am going to catalog the breakdown for the other issue and move these over. Since this is step 2 I'd advocate we split the files out by api.php
Edited: here is a direct link to the hooks in core and the suggested name for the class / file. https://www.drupal.org/files/issues/2025-03-17/hookOrganization.txt
Comment #29
mstrelan commentedIf we split by api.php it will look like this:
NodeConfigTranslationHooks
Hooks: configTranslationInfoAlter
Services: n/a
NodeCoreHooks
Hooks: cron
Services: module_handler, entity_type_manager, state
NodeDatabaseHooks
Hooks: queryNodeAccessAlter
Services: current_user, module_handler, entity_type_manager, node.grant_storage, renderer
NodeEntityHooks
Hooks: userPredelete, configurableLanguageDelete, commentInsert, commentUpdate, commentDelete, entityViewDisplayAlter, entityExtraFieldInfo, nodeAccess
Services: entity_type_manager
Note: Only the first two hooks need the service
NodeHelpHooks
Hooks: hook_help
Services: current_user, messenger
NodeMenuHooks
Hooks: localTasksAlter
Services: n/a
NodeModuleHooks
Hooks: modulesInstalled, modulesUninstalled
Services: moduleHandler
NodeNodeHooks
Hooks: ranking
Services: state
NodeThemeHooks
Hooks: pageTop, formSystemThemesAdminFormAlter, theme
Services: current_route_match, form_builder, config.factory
Note: pageTop doesn't require config.factory but it would be injected pretty much everywhere. Also theme has no requirements.
NodeUserHooks
Hooks: userCancelBlockUnpublish, userCancelReassign
Services: entity_type_manager, module_handler
So we will be splitting to 10 new classes. Some of these make a lot of sense, like NodeModuleHooks. But NodeEntityHooks looks like it could benefit from splitting further, e.g. userPredelete could go to NodeUserHooks so we dont need entity_type_manager there.
I don't really have an opinion on the matter, but if it's agreed this is what we should do then I can work on it.
Comment #30
nicxvan commentedWow that was quick!
I think that looks great!
That would be part of step 3.
I don't have a super strong opinion, but I think keeping it to three steps will reduce bike shedding while getting the improvements we want in a reasonable time.
For example, I wouldn't put those with user hooks since they are not part of that, I'd make it part of another class like: NodeEntityHooks and NodeEntityDeleteHooks, then I would add the three delete hooks even if only two need the injection.
Comment #31
smustgrave commentedJust throwing my 2cents in but I really like how #3502014: Clean up hook implementations in the Taxonomy module got organized. Maybe can do something similar.
Comment #32
nicxvan commented29 is following the same logic as #3502014: Clean up hook implementations in the Taxonomy module
Comment #33
smustgrave commentedLet’s do that then :)
Comment #34
quietone commentedComment #35
quietone commentedNevermind. I talked with larowlan and found out I thought this was not already in a minor release. sorry for the noise.
Comment #36
acbramley commentedComment #40
acbramley commentedSplit by api.php as per #29
Comment #41
acbramley commentedComment #42
acbramley commentedFYI I haven't changed anything to do with DI except in NodeModuleHooks where both hooks need the module handler.
I'm not sure how we should handle it, we've seen impacts to performance tests in previous changes and I've also seen how they can break Kernel tests where a test doesn't install the module that implements the service being injected, since the test doesn't hit the hook it's not necessary.
Maybe we should defer the DI discussion to another issue and get the organisation of hooks in first since it's going to be quite disruptive already.
Comment #43
smustgrave commented@acbramley I don't think DI have been blocker for these tickets. @nicxvan may know the ticket where that's being tracked?
Sounds like good novice tasks for a dev day to go back and add DI to these conversions!
1 comments I do want to make, not 100% phpstan and all is running on hook folders yet but will it ding the empty docs for __construct()? Not a blocker just mentioning.
Looking at the MR seems like a good conversion though
Comment #44
nicxvan commentedConstructors do not need documentation if everything is typed I think.
I agree DI should not be a blocker.
Comment #45
longwaveSome bikeshedding in my review comments.
Comment #46
longwaveComment #47
smustgrave commentedSo should this one and the taxonomy one be postponed till an agreed upon structure can be decided?
Comment #48
needs-review-queue-bot commentedThe 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.
Comment #49
acbramley commentedRebased this and fixed up a bunch of conflicts, copying across some changes to hook_help that made it in in the meantime.
Comment #50
smustgrave commentedFeel this is one could be endless chasing perfection. Believe what's currently there is definitely better then present. If a standard (apologize if one has been made and I don't know) think this is good.
Going to go on a limb and mark.
Comment #51
needs-review-queue-bot commentedThe 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.
Comment #52
acbramley commentedComment #53
smustgrave commentedRestoring from #50
Comment #54
berdirAdded a few thoughts, not changing the status for now if you want to wait for core committer feedback/decisions.
Comment #55
acbramley commentedI've consolidated that feedback, also helps with DI so this is a good improvement. I agree that splitting by API should be the guideline but not a strict rule. We really do need some standards around it though because we have some wildly different approaches popping up in core already (see file module for example)
Would be good to get this in sooner rather than later so I don't have to rebase it again!
Comment #56
nicxvan commentedJust to be clear the per api file was meant to make these refactors easier, step three was always going to be logical groupings.
It was also meant to make rebasing easier.
The length of time these are taking is really making me reconsider.
Neither taxonomy nor node are in yet despite fairly active tracking.
I wonder if we need a more coordinated strategy with these.
Comment #57
mstrelan commentedI wanted to circle back to #17 from @catch:
The standout for me is
hook_page_topwhich is invoked everywhere, and now gets 4 services injected. Probably RouteMatch, Renderer and EntityTypeManager would be instantiated anyway, but maybe not FormBuilder. I don't know if this is significant enough to worry about.Comment #59
catchI've added a link to #57 on #3540386: [meta] Explore using more service closures to break deep dependency chains and load fewer services, I think we can handle it as a spin-off of that issue rather than this one.
But also I really wondered why that page-specific logic has to be implemented in hook_page_top() at all, so I searched to see if there was an issue about it, and found this one I opened in 2023 :/ #3339905: Add page_top and page_bottom to #attached to get rid of NodeThemeHooks::pageTop().
Given there's at least two possible ways we could ensure that form builder doesn't get instantiated on every request by that hook, let's just go ahead here.
fwiw I agree with moving cron/ranking to NodeSearchHooks - looks a lot tidier.
Committed/pushed to 11.x, thanks!
Comment #61
acbramley commentedThanks so much @catch
Comment #62
nicxvan commented