OOP hooks using event dispatcher

Created on 19 April 2024, 2 months ago
Updated 1 June 2024, 23 days ago

Problem/Motivation

For a very long time we wanted to introduce hooks in some OOP manner. The following goals altogether however have proven elusive

  1. No magic naming in the implementations.
  2. A hook implementation should be simple and have minimal boilerplate.
  3. Calling any hook without defining anything should be possible as it is now.
  4. Reordering hooks should be doable.
  5. Relative reordering should be easier. ckeditor5_module_implements_alter is ouch.
  6. Minimal added code to core.

Proposed resolution

Big kudos to EclipseGc for realizing the Symfony EventDispatcherInterface has a getListeners method which allows us to call listeners any way we want. Unlike dispatch this allows us to call listeners with any arguments we want without defining an event object. dpi pioneered attributes for hooks on Hux. I also advocate for attributes but simpler. In short:

namespace Drupal\node\Hook;
class NodeHooks {
  #[Hook('user_cancel')]
  public function userCancel($edit, UserInterface $account, $method) {

In detail:

  1. Hook implementations go into Drupal\modulename\Hook namespace (subdirectories work too). Familiar pattern from plugins. These are automatically registered as autowired services with the class name as the service id. This makes for minimal boilerplate. If autowire doesn't suffice -- should be very rare -- they can be registered manually as well, the service id is the class name.
  2. Hook implementations needs to be marked with a Hook attribute. This is new. Either on a method #[Hook('user_cancel')] or on the class #[Hook('user_cancel', method: 'UserCancel')]. If the class Hook doesn't specify a method, __invoke is the default method.
  3. The attribute supports a priority as well: #[Hook('user_cancel', priority: -20)]. The priority defaults to such values as to keep module order.
  4. The edge case of "implementing a hook on behalf of another module" is also supported by simply specifying the module in the attribute. It defaults to the defining module.
  5. This attribute is patterned on the Symfony AsEventListener attribute which is shipped with the event dispatcher but it is only used in the full Symfony framework.
  6. Multiple implementations are totally allowed on multiple axis: one method can implement multiple hooks by adding a Hook attribute for each. One module can implement a hook as many times as it wants in as many classes as it wants. This allows, for example, adding form_alter implementations firing on other conditions than form id without touching any existing implementation. For now I exempted the hooks fired via ModuleHandler::invoke from this, documentation would be needed for those, luckily very few such are used runtime: library_build_info, mail and help. For example, help expects a string return, it's not even clear what multiple implementations could mean in this case. Also, ModuleHandler::invoke is used as a replacement for a failure-tolerant $function() call, once again it's not at all clear what multiple replacements could mean.
  7. All the implementations are stored in a container parameter. This is a multidimensional array with keys for hook, class, method and the values are module and priority. To reorder, use a service provider alter and change the priority. A proof of concept of how ckeditor5_module_implements_alter will look after is provided. I also added a helper method which retrieves all the priorities for a given hook to make absolute ordering easier for now. Before/after is certainly a possibility in a followup.
  8. Old style procedural calls are integrated into the new system. Separate hook caching is removed, everything is now stored in the container. I suggest contrib and custom could start converting hooks to new style and have the procedural implementation call the new style which is, again, available as a service. The attribute supports a "replaces" key for this purposes. This allows maintaining one hook implementation code while being compatible with a wide array of Drupal versions. I suppose since interfaces for autowire got added for D10.1, the floor would be D10.1 which is just as well, I believe D10.0 is out of support already.
  9. Not much code is needed and a lot of is BC: due to the vast simplification of ModuleHandler, core/lib/Drupal/Core only becomes 72 lines longer. This is enough for the new attribute, gathering all the implementation data, registering them as event subscribers and firing them as needed. Once the BC layers are removed, it'll be significantly negative. Luckily we didn't need to provide any new facility for hook_module_implements_alter() as service provider alter can do it nor any sorting because event dispatcher does that.
  10. If loading all hook classes at build time becomes a problem we already have an issue for that: 📌 Investigate possibilities to parse attributes without reflection Active .
  11. For API documentation purposes, it is possible to create a separate attribute class for each and move the doxygen to the constructor. An example of such an attribute is provided in HookFormAlter (without moving the doxygen for now).

The attached PR contains a PoC for a conversion of ckeditor alter, one node hook manually converted showing how autowire would work. I have a quick-and-dirty conversion script which eventually needs to become a rector rule but it allows interested parties to see the future.

Remaining tasks

  1. A framework manager needs to review per 61.
  2. While more tests could be written, the difficulty of getting the current test harness to pass proves there is already ample testing already. This is a decision for the community to make.

User interface changes

API changes

Oh my.

Data model changes

Release notes snippet

📌 Task
Status

Needs review

Version

11.0 🔥

Component
Base 

Last updated about 2 hours ago

Created by

🇨🇦Canada Charlie ChX Negyesi 🍁Canada

Live updates comments and jobs are added and updated live.
  • Needs framework manager review

    It is used to alert the framework manager core committer(s) that an issue significantly impacts (or has the potential to impact) multiple subsystems or represents a significant change or addition in architecture or public APIs, and their signoff is needed (see the governance policy draft for more information). If an issue significantly impacts only one subsystem, use Needs subsystem maintainer review instead, and make sure the issue component is set to the correct subsystem.

Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @Charlie ChX Negyesi
  • Merge request !7604hook attempt → (Open) created by Unnamed author
  • Pipeline finished with Failed
    2 months ago
    Total: 188s
    #150964
  • Pipeline finished with Canceled
    2 months ago
    Total: 53s
    #151036
  • Pipeline finished with Canceled
    2 months ago
    Total: 80s
    #151040
  • Pipeline finished with Canceled
    2 months ago
    #151042
  • Pipeline finished with Failed
    2 months ago
    Total: 208s
    #151044
  • 🇬🇧United Kingdom joachim

    > Hook implementations go into Drupal\modulename\Hook namespace. Familiar pattern from plugins.

    +1, good DX.

    > These are automatically registered as autowired services

    Do they specifically need to be registered as services? They don't need to be used as services, and that's going to add a LOT of bloat to the container.

    > All the implementations are stored in a container parameter

    Likewise - that's a lot of bloat, surely?

    > The attribute supports a priority as well: #[Hook(hook: 'user_cancel', priority: -20)]

    I'm torn between using priority to be consistent with Symfony, and suggesting we use weights to be consistent with Drupal-land.

    > There's little to no concern given to backwards compatibility. New hook implementations fire after the old ones and they are altered and fired completely separately. It is not possible to mix old and new

    That would be doable if we wanted to, though, I think? Have something that discovers procedural hooks, and registers them with the OOP hook handler using the module weight.

    We could even smooth the path to conversion by allowing #[Hook] to be used on a procedural function if we wanted, no?

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    > Do they specifically need to be registered as services?

    Well, yes, we use the event dispatcher to fire them and the dispatcher uses services.

    Note the full Symfony framework will also register every class it finds under a specified directory which is usually just App\ so that system is designed to take it. Surely by now we would if our array dumper/loader couldn't but can't see why not.

    > I'm torn between using priority to be consistent with Symfony, and suggesting we use weights to be consistent with Drupal-land.

    You and me, brother. I have no idea what to do here. This is up to community to decide. Code wise, it's trivial to change: change string replace priority to weight and a - sign in HookCompilerPass because the event dispatcher does use priority but that's an internal detail, really.

    I neither know nor do I want to figure out how to convert procedural functions into something that the event dispatcher can fire. The entire system is designed around hook implementations being methods on objects. But I have a suggestion. Let's pretend we have a dumper which can dump the service definitions written by autowire. Then:

    1. Convert the hook code to the new system
    2. Dump the service definition generated and add it to service yml
    3. Change the procedural call to manually call the converted code
    4. Add a [#HookConverted] attribute to the procedural code. This would be ignored completely by pre-patch Drupals and this patch could change ModuleHandler to skip functions marked so.

    This would allow modules to convert to the new system yet remain compatible with old Drupals without maintaining the same code twice. Ordering would need to be doubled up if this is what the community wants to do. Not my call.

  • Pipeline finished with Failed
    2 months ago
    Total: 243s
    #151203
  • Pipeline finished with Canceled
    2 months ago
    Total: 36s
    #151207
  • 🇬🇧United Kingdom longwave UK

    >> These are automatically registered as autowired services

    > Do they specifically need to be registered as services? They don't need to be used as services, and that's going to add a LOT of bloat to the container.

    If they are registered as private services they don't bloat the container as much; during container build they should be inlined into the event dispatcher definition as that is the only place they are needed, they don't need to be publically accessible by any name.

  • Pipeline finished with Failed
    2 months ago
    #151208
  • 🇬🇧United Kingdom longwave UK

    > Note the full Symfony framework will also register every class it finds under a specified directory which is usually just App\ so that system is designed to take it. Surely by now we would if our array dumper/loader couldn't but can't see why not.

    Symfony does do this but also services are private by default, and unused private services are then optimised away - so if you don't refer to an automatically discovered class somewhere, it doesn't even exist in the final container. Drupal is still public-by-default and we need to unwind a lot of things to be able to change that.

  • Pipeline finished with Failed
    2 months ago
    Total: 200s
    #151339
  • Pipeline finished with Canceled
    2 months ago
    Total: 86s
    #151353
  • Pipeline finished with Canceled
    2 months ago
    Total: 88s
    #151354
  • Pipeline finished with Failed
    2 months ago
    Total: 105s
    #151356
  • Pipeline finished with Failed
    2 months ago
    Total: 178s
    #151358
  • Pipeline finished with Failed
    2 months ago
    Total: 178s
    #151364
  • Pipeline finished with Failed
    2 months ago
    #151377
  • Pipeline finished with Failed
    2 months ago
    Total: 189s
    #151510
  • Pipeline finished with Success
    2 months ago
    Total: 959s
    #152650
  • Pipeline finished with Canceled
    2 months ago
    Total: 780s
    #152862
  • Pipeline finished with Success
    2 months ago
    Total: 987s
    #152867
  • Pipeline finished with Failed
    2 months ago
    Total: 654s
    #153030
  • Pipeline finished with Success
    2 months ago
    Total: 988s
    #153057
  • 🇬🇧United Kingdom joachim

    > A method can have multiple Hook attributes if it implements multiple hooks. For example, node_comment_insert and node_comment_update have the exact same implementation and so they could become

    This is nice!
    But how would the implementation know which hook is being invoked currently?

    We also need to think about how this should affect api.php file documentation. When Hook() attributes become the default way of implementing a hook, an api.php file containing procedural functions won't make much sense.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    But how would the implementation know which hook is being invoked currently?

    They don't. This feature is for code that doesn't care. Please check node_comment_insert and node_comment_update -- they are literally the same code.

  • 🇺🇸United States DamienMcKenna NH, USA

    But how would the implementation know which hook is being invoked currently?

    Isn't it up to the person writing the code to make it aware of what is triggering the code? Or am I missing something?

  • 🇩🇪Germany Fabianx

    I apologize if I am missing something, but:

    #[Hook(hook: 'user_cancel')]

    feels a little bit duplicated (or is that because of alter hooks being different?).

    But I would prefer clearly:

    #[Hook('user_cancel')]
    #[AlterHook('module_implements')]
    
    or
    
    #[AlterHook('module_implements_alter')]
    

    Therefore we get rid of the magic naming, but don't have: Hook(hook: ) duplicated.

    But maybe it is necessary, then please disregard.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada
    1. I am not sure how this works. It's entirely possible you can already drop hook: -- if it just calls the constructor then you already can. I pushed a commit doing this.
    2. We could add an AlterHook attribute extending Hook which adds _alter and ... that's it. So Hook('foo_alter') and AlterHook('foo') is the exact same. Is this acceptable?
    3. Regarding procedural, this is described in the issue summary: I advocate against the complexity of adding a harness to call the old ones from the new system but of course that's just me. But I also wrote it up in #8 how to integrate them seamlessly using a ProceduralCall class and a magic __call method call whatever procedural call it is passed to.
    4. Also I am not seeing the event based hooks take care of module implements alter -- this is explained in the issue summary and an example is provided in the PR. The existing service provider infrastructure is used.
  • Pipeline finished with Success
    2 months ago
    Total: 1020s
    #153597
  • 🇬🇧United Kingdom catch

    You and me, brother. I have no idea what to do here. This is up to community to decide. Code wise, it's trivial to change: change string replace priority to weight and a - sign in HookCompilerPass because the event dispatcher does use priority but that's an internal detail, really.

    There's an old issue at 📌 Add before/after ordering to events Needs work , which with new things, could maybe look like #[Before('whatever')] #[After('whatever')] #[Replaces('whatever') (or an extra param to the hook attribute) then numeric priorities/weights wouldn't need to be interacted with by modules.

    Isn't it up to the person writing the code to make it aware of what is triggering the code? Or am I missing something?

    It's for cases where exactly the same things needs to happen. i.e. sometimes you have identical code running on insert or update hooks, right now you'd put that in a extra method/function somewhere and call it from both, with being able to annotate the same method for multiple hooks, everything is in one place.

    If the implementation needs to be different, then it should two methods with their own hook attributes.

  • 🇬🇧United Kingdom catch

    So Hook('foo_alter') and AlterHook('foo') is the exact same. Is this acceptable? Would look like like

    I think this might have been discussed in one of the other issues (or even implemented in @donquixote's issue). Alter hooks are special so it's worth doing dedicated for it IMO.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    I am all in for adding before / after into the attribute, I see zero challenges here, HookCompilerPass could run the (already documented) min/max as needed, but could we punt this into a followup? My hand is up, I will write it, no problems. I just would like to proceed in minimal slices so we can proceed in parallel after. Likely the conversions made in this issue shouldn't be merged either, they are there to ferret out any problems (and they did).

    Because while I see no technical challenges, the exact syntax likely will take quite a bit of discussion to figure out -- is it before/after a module? a list of modules? a list of class-method pairs? how do we indicate I want to be first/last? what if two does say want that? what if the ordering is not doable? A wants to be after B, B wants to be after C, C wants to be after A, opsie. So there's a bit to discuss on that front.

  • Pipeline finished with Failed
    2 months ago
    Total: 165s
    #153672
  • 🇬🇧United Kingdom catch

    I am all in for adding before / after into the attribute, I see zero challenges here, HookCompilerPass could run the (already documented) min/max as needed, but could we punt this into a followup?

    Yes but I think it means we can defer some discussion about weights vs. priorities too.

  • Pipeline finished with Success
    2 months ago
    Total: 1108s
    #153836
  • Status changed to Needs work 2 months ago
  • 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.

  • 🇩🇪Germany Fabianx

    One thing I liked about the Hux-style approach was that you could just add a Hook attribute to any service and it was not limited to things in the hook namespace - you just tag the service as a 'hook_listener' and it works.

    BUT this can and should be a follow-up.

    I just want to ensure that this use case CAN still be made to work with the current architecture. (even if we then just later decide for or against it)

  • 🇩🇪Germany Fabianx

    Patch looks great so far, but if it was me:

    - We only allow one hook implementation per module right now (which is fine)

    Why don't we just merge the collected listeners with the procedural when:

    moduleHandler->invokeAll() collects the procedural implementations?

    Then the whole module_implements_alter(), etc. would still work (without any changes or before/after!).

    That would ensure for this FIRST iteration, we get OOP hooks via event listeners and auto-wired services, but moduleHandler really does not change except for discovering these implementations for the hook.

    Before this was not possible due to how we did not have getListeners() so ordering became impossible, but now it's natural.

    And we still throw an Exception() if a hook is defined more than once in the same way.

    I feel - and I might be wrong - this would make for the most minimal patch.

  • 🇫🇷France andypost

    Good idea to figure out what can fit into 10.3.x (deadline - week 29 Apr'24)

    Current state looks good starting point but Experimental API needs more discussion

    PS: looking at NodeNooks.php implantation it reminds me Action plugin, for example \Drupal\user\Plugin\Action\RemoveRoleUser

  • Pipeline finished with Failed
    about 2 months ago
    Total: 537s
    #157197
  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    I posted a new version folding procedural hooks in resulting in much simplified ModuleHandler -- and the build time parts are not so bad either. We discussed with catch and just throwing in every function as a hook candidate seems a very reasonable plan as already there are not that many functions which are not hooks.

    A few tests around install and update are not passing yet, the rest are happy.

    The only unrelated change I made is renaming core_field_views_data to views_field_views_data. But since that's a hook implementation I doubt it's a problem. If not desirable, we can revert -- it will work now but when I made the change ModuleHandler::invoke was stricter, requiring $module to be a $module and $hook to be a $hook. Since then I dropped this for more backwards compatibility.

  • Pipeline finished with Failed
    about 2 months ago
    Total: 549s
    #157274
  • 🇺🇸United States nicxvan

    I'm not sure if this is out of scope, but I don't see any special handling for .theme files.
    I do see the update for where hook_theme() is invoked in registry, but while there is some code to parse in HookHelper for .module, .inc, and .install, I don't see the same for .theme.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    I would punt on themes. While ThemeHandler does add the themes to the PSR-4 namespaces and so it is maybe possible to give themes a similar treatment I would like to proceed in as small chunks as possible to make it easier to get this in. Please note DrupalKernel currently has zero to do with themes and we would need to figure out some sort of theme namespaces during container build which might raise some issues: the patch currently has failures around installation, I am afraid it make even this worse.

  • 🇺🇸United States nicxvan

    Fair enough, I'm ok with that scope, we should be sure to include that carve out in the change record so there is no confusion.

    Another note from the slack conversation between Chx, godotislate and myself is the following from godotislate:

    it looks like they are all invoked after module handler has its turn so keeping them OOS seems fine

    So they are likely not intermingled and can be modified in another issue.

  • 🇺🇸United States nicxvan

    Just gathering the test failures:

    Drupal\Tests\demo_umami\Functional\DemoUmamiProfileTest
    Drupal\Tests\media\Functional\UrlResolverTest
    Drupal\Tests\system\Functional\UpdateSystem\UpdatePathLastRemovedTest
    Drupal\Tests\system\Functional\UpdateSystem\UpdateScriptTest
    Drupal\Tests\standard\Functional\StandardTest
    Drupal\Tests\demo_umami\FunctionalJavascript\OpenTelemetryAuthenticatedPerformanceTest
    Drupal\Tests\standard\FunctionalJavascript\StandardPerformanceTest
    
  • Pipeline finished with Failed
    about 2 months ago
    Total: 656s
    #157818
  • Pipeline finished with Canceled
    about 2 months ago
    Total: 270s
    #157951
  • Pipeline finished with Failed
    about 2 months ago
    Total: 508s
    #157954
  • Pipeline finished with Failed
    about 2 months ago
    #157962
  • Pipeline finished with Failed
    about 2 months ago
    Total: 2602s
    #157970
  • 🇺🇸United States nicxvan

    The cache test is passing for me locally, I am going to try rebasing.

  • 🇺🇸United States nicxvan

    It was already up to date, I'm not sure what is going on, the test passes locally, but it is failing upstream.

    I narrowed it down to this assertion in: core/modules/block/tests/src/Functional/BlockCacheTest.php

    // Block content not served from cache.
    $this->drupalLogout();
    $this->drupalGet('user');
    $this->assertSession()->pageTextContains($current_content);

  • 🇦🇺Australia acbramley

    It's great to see more movement on this problem space but this new issue has raised a few questions for me. Quick background: I have been using Hux since it's inception, and am arguably one of its biggest users and supporters seeing as my colleague wrote it :)

    We now have several implementations of Hux like OOP hooks, it would be great to get a consensus on what one we're going forward with and close out the other MRs/issues ( https://www.drupal.org/project/drupal/issues/2237831 🌱 Allow module services to specify hooks Needs work for example)

    I'm also curious what the differences (if any) there are with this solution when compared to Hux. It seems quite similar but I haven't had a chance to review the code yet.

    Thanks for all the great work so far!

  • Pipeline finished with Failed
    about 2 months ago
    Total: 192s
    #158335
  • Pipeline finished with Failed
    about 2 months ago
    Total: 538s
    #158348
  • Pipeline finished with Failed
    about 2 months ago
    Total: 573s
    #158352
  • Pipeline finished with Failed
    about 2 months ago
    Total: 508s
    #158360
  • Pipeline finished with Success
    about 2 months ago
    Total: 507s
    #158368
  • Pipeline finished with Success
    about 2 months ago
    Total: 497s
    #158407
  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    @acbramley that decision is not mine to make. As far as I can tell, there are two things Hux allows for and the current PR does not and both are easy to add either here if it's blocking or in a followup if desired. I do not yet see the need but I am easy to convince.

    1. hooks in subdirectories -- we already have multiple classes, do we need multiple directories too? I can imagine arranging into say NodeUserHooks.php and NodeTermHooks.php but ... subdirectories? Are we really going to need them? I can write them, sure, we already have a recursive iterator, using another is not a biggie.
    2. hooks as tagged services -- I think the Hook attribute on a class completely replaces this need. If I am wrong, let me know and I will add this.

    Re-reading Hux after writing this was certainly useful because I quickly added the replaces functionality.

    This PR has some advantages over Hux. Apologies if I missed some of its functionality.

    1. Complete subsuming of procedural hooks resulting in a simplification of ModuleHandler by moving some of the code to discovery time.
    2. Support for the rather complex alter ordering for multiple types. I think my support is easier to understand than the current one, even.
    3. Speaking of ordering, an equivalent for module implements alter is supported -- without adding any new mechanism even. Just use service provider alters on the new container parameter.

    For the future, this patch delegates the actual sorting to the event dispatcher allowing for a followup which adds before/after functionality benefiting both events and hooks automatically.

  • Status changed to Needs review about 2 months ago
  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    While more tests certainly need to be added this is now green and certainly ready for review.

  • 🇬🇧United Kingdom catch

    @acbramley I've gone ahead and postponed 📌 Hux-style hooks, proof of concept Needs work which has a similar API but ended up with a very complicated bc layer, the discussion on that issue is extremely relevant to here still and we should copy credit over etc. It could also use a proper comparison of the different approaches between these two issues somewhere here, but not going to attempt that on Sunday morning..

    I've also postponed 🌱 Allow module services to specify hooks Needs work which didn't have any recent code but was the original, ten year old discussion, that wanted what we ended up with here but didn't have Symfony 6 and attributes (or the experience of Hux) to make it a reality.

    Between all the various issues, and can't separate my own very strong opinions here, I think there is the following in common between the two active issues that so far seems non-controversial:

    • Allow OOP hooks to be invoked with the current hook invoking APIs, so that conversions to OOP hooks can be done independently from any changes to invocation.
    • Continue to support passing 'arbitrary' arguments to hooks, so that e.g. node load implementations type hint NodeInterface $node and get that, not an Event with a ::getNode() method.
    • #[Hook] attribute possibly with #[HookAlter] variation, and autowiring, so that hooks can still be defined within a single method in a single file (no requirements for separate YAML or ::getSubscribedEvents()).
    • Allow multiple hook attributes on a method (for the insert/update identical logic case).
    • Allow multiple classes for hooks so that they can inject only the services they need.

    Things that are still to be defined somewhat and may end up in follow-ups:

    • Allowing multiple implementations of the same hook in one module vs. not.
    • Hook weights vs. priorities vs. before/after/replaces support, how much bc there is for hook_module_implements_alter(), and what any eventual replacement for hook_module_implements_alter() might look like (can we do it all with before/after/replaces attributes + a compiler pass?)
  • 🇨🇭Switzerland Berdir Switzerland

    catch also tagged 📌 Replace hook_cron() with a more modern approach Needs work as related. Being able have multiple hooks in a module would definitely solve one aspect that the cron subscriber issue is trying to solve, another is the ability to have cron subscribers in core components. Would this allow to also basically treat each component as a "module"? That would allow to move a lot of the code that's currently in system.module, especially hooks, to the respective component. Similar to how we already support plugin discovery there, each core component could have a Hooks folder as well.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    Supporting the full container.namespaces instead of module namespaces to include core is certainly easily doable.

  • 🇬🇧United Kingdom joachim

    How will hooks whose names include a variable work?

    E.g. hook_form_FORM_ID_alter, hook_ENTITY_TYPE_update()?

    Could the variable part be a separate value in the attribute?

  • 🇬🇧United Kingdom catch

    @joachim I would think that would work the same as now? - i.e. hook_form_FORM_ID_alter() is always implemented as hook_form_my_form_id_alter() same as hook_ENTITY_TYPE_load() is the same as hook_node_load(). If you want to operate on all entities or all forms, then you go back to hook_form_alter() and hook_entity_load()

  • 🇬🇧United Kingdom joachim

    What I meant is, would we have:

      #[Hook('hook_form_myform_alter')]
    

    or split this out to a parameter:

      #[Hook('hook_form_FORM_ID_alter', form: 'myform')]
    
  • Status changed to Needs work about 2 months ago
  • 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.

  • 🇦🇺Australia acbramley

    Big +1 to just having it as it is now (i.e #[Hook('hook_form_myform_alter')], not #[Hook('hook_form_FORM_ID_alter', form: 'myform')] - I don't even know how that'd work...

  • 🇨🇭Switzerland Berdir Switzerland

    > #[Hook('hook_form_FORM_ID_alter', form: 'myform')]

    I don't think your proposal makes it any easier to use, it's longer, and you still have to remember the pattern, which tends to be confusing as many forms have a "form" in their form_id (is it form_FORM_ID_alter() or FORM_ID_form_alter()...)

    What we *could* add is something like a #[FormAlter('form_id')] or #[EntityHook(entity_type: 'node', hook: 'presave')], specific attribute classes that then build the specific hook name patterns that they know about. That could easily be a follow-up, the discovery already explicitly supports any Hook subclass (for example the already implemented AlterHook), here can be as many as we want, you could even add your own.

  • 🇬🇧United Kingdom joachim

    > #[Hook('hook_form_FORM_ID_alter', form: 'myform')]
    > #[FormAlter('form_id')]

    I'm not hung up on the specific syntax, the point was splitting out the variable part for the FORM_ID placeholder into a separate value.

    Happy to leave it as a follow-up to keep this issue simple.

  • 🇬🇧United Kingdom catch

    We have hook_form_BASE_FORM_ID_alter() too, so any special handling would need to also handle that.

  • 🇦🇺Australia mstrelan

    We could take that one step further and turn every hook into its own attribute that extends the base hook class. In some ways that would be nice because you could just use #[Install] for hook_install, or #[Cron] for hook_cron, which removes the magic hook name string, but it would also be fairly annoying that every module would need to declare a whole bunch of hook attributes. Although, this could replace mymodule.api.php so maybe it has some merit.

  • 🇬🇧United Kingdom joachim

    Oh yes, we need to rethink MODULE.api.php as part of this issue. Otherwise it's not going to make sense.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    mstrelan , that's a wonderful idea. Custom code could still just use #[Hook] for an undocumented one-off hook if needed for quick-and-dirty -- the ability to keep the zero fuss on either caller is implementation side is of extreme importance to me (indeed that's why I jumped on this). But clean code could just use create a custom attribute class and put the API documentation on the constructor. A somewhat small change to api.drupal.org, hopefully. I presume it would still all extend the Hook attribute just set the hook name to something relevant. We could use a class constant for that, so it would become ->invokeAll(Cron::HOOK) and #[Cron] on the implementation side which would be the exact same as >invokeAll('hook') and #[Hook('cron')]. I really like this idea.

  • 🇬🇧United Kingdom longwave UK

    Re form alters once we've stopped using procedural hooks we could just use the form class name as the form ID, and the ::class constant in hooks, I think? Yo start with this would only be for new forms but we could eventually deprecate form IDs and add some form of BC, maybe.

  • 🇬🇧United Kingdom joachim

    > So if you have a fixed hook then you could just create a attribute class and put the API documentation on the constructor. A somewhat small change to api.drupal.org, hopefully

    Harder to discover by reading code if hook attributes are mixed in with other kinds of attribute in src/Attribute.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    Oh, that's easy to solve, just put them in src/Attribute/Hook.

  • With the possibility of additional/contrib/custom hook classes, it might make sense to add HookInterface and make every relevant attribute class implement that. With the Interface definining some basic setters and getters (setHook(), getHook() etc..

    Also, and I haven't thought through the complexities, I wonder if there are any advantages to making Hook attributes support multiple hooks, such as node_update + node_insert in one hook attribute, or form_node_form + form_media_form in one alter hook attribute?

    So

    #[Hook('node_update')]
    #[Hook('node_insert'])]
    public function doNodeStuff(...) {}
    

    becomes

    #[Hook(['node_update', 'node_insert'])]
    public function doNodeStuff(...) {}
    

    or

    #[AlterHook('form_node_form')]
    #[AlterHook('form_media_form')]
    public function alterNodeOrMediaForm(...) {}
    
    #[AlterHook(['form_node_form', 'form_media_form'])]
    public function alterNodeOrMediaForm(...) {}
    
  • 🇬🇧United Kingdom longwave UK

    #59 feels like a complication we don't need for now, multiple attributes are supported and feel much cleaner to read.

  • 🇬🇧United Kingdom longwave UK

    Tagging for framework manager signoff as this is a significant addition to an existing API (or set of APIs depending on how you look at it)

  • I'm also not sure we need an interface if everything must descend from the base Hook class anyway.

    I don't have any use cases in mind, but the idea for an interface would be to allow some degree of future proofing, in case the hook attribute class properties need amendment at some point.

    It would also allow other hook attribute classes to do some logic in getters and setters instead of directly acting on property values.

    Note that I don't feel strongly about this, but I thought I'd mention it in case.

  • #[FormAlter(FormTestAlterForm::FORMID)]

    This would probably be better as #[FormAlter(FormTestAlterForm::FORM_ID)], but I like the idea of adding FormAlter.

    I'm not sold on removing Hook from the attribute name. I like being able to see at a glance which attributes are for hooks. We could conceivably have other attributes on functions that aren't for hooks, and that would make it harder to see which ones are hooks.

  • Pipeline finished with Failed
    about 2 months ago
    Total: 239s
    #162027
  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    While I did resurrect ModuleHandlerTest it is an extremely strongly coupled unit test which has assumptions about how the implementation exactly works. In particular, ::testInvokeAll presumes addModule followed by invokeAll loads the module while ::testLoadModule asserts addModule does not load the module. There is no rhyme or reason to presume addModule doesn't load the module and so now it does. The system proposed here can't work otherwise because module_implements_alter needs to be called build time and it would be inconsistent if we only loaded the modules implementing it. Since addModule is a very rare, install time only thing, I see no harm in this change.

  • Pipeline finished with Failed
    about 2 months ago
    Total: 198s
    #162033
  • Pipeline finished with Failed
    about 2 months ago
    #162038
  • Pipeline finished with Failed
    about 2 months ago
    #162054
  • Pipeline finished with Failed
    about 2 months ago
    Total: 514s
    #162183
  • Pipeline finished with Failed
    about 2 months ago
    Total: 506s
    #162191
  • Pipeline finished with Failed
    about 2 months ago
    Total: 203s
    #162446
  • Pipeline finished with Failed
    about 2 months ago
    #162453
  • Pipeline finished with Failed
    about 2 months ago
    Total: 511s
    #162476
  • Pipeline finished with Success
    about 2 months ago
    Total: 542s
    #162488
  • Status changed to Needs review about 2 months ago
  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    I just rebased this so it applies.

  • Pipeline finished with Failed
    about 2 months ago
    Total: 497s
    #162579
  • Pipeline finished with Success
    about 2 months ago
    Total: 704s
    #163396
  • Pipeline finished with Success
    about 2 months ago
    Total: 548s
    #163406
  • Pipeline finished with Success
    about 2 months ago
    Total: 608s
    #164567
  • 🇺🇸United States nicxvan

    I'm wondering if the merge request being in draft is intentional?

    My understanding is that this is ready for a deeper review with the following comments that all need to be addressed.

    1. A framework manager needs to review per 61.
    2. The suggestion to change FormAlter(FormTestAlterForm::FORMID) to FormAlter(FormTestAlterForm::FORM_ID) per 63.
    3. One question in the MR about handling the profile extension that I think may be addressed.
    4. The question in the issue summary about whether additional tests are needed.

    Let me know if I missed anything, I added these to the issue summary too.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    Thanks for the meta review :)

    1. It's in draft because I have no idea what that means. I am just writing patches, here (in two weeks it'll be twenty years). This new workflow is a complete unknown for me.
    2. I talked with catch about a review , likely next week.
    3. I will
    4. Profile was addressed I thought?
    5. That's part of #1 I think

    Edit: sorry I didn't see the review by solideogloria I will process that too.

  • @ghost-of-drupal-past If you click on the Merge Request (and make sure you are signed in to GitLab; just click Log In and it will automatically auth you), there should be an option to mark the MR as "Ready". That will remove the Draft status.

    In addition, there should be a way to mark each of my comments as "resolved", either now or after you have committed a change that addresses a comment.

  • For Drupal core MRs, I don't know that marking them as "draft" really matters much, but often on other projects, reviewers are accustomed to skip reviewing draft MRs/PRs, because it's a signifier of WIP product not ready for review or to merge.

    FYI, if you are the MR author, you can move it out of draft by clicking on the kebab menu in the upper right and selecting "Mark as ready".

  • Yes. Also, only the author can select "Mark as ready", I think, so it would be a good idea to do so.

  • 🇺🇸United States nicxvan

    Thanks guys. As far as the profile question it is resolved, I was on my phone so I couldn't see the changes super accurately.

    collectModuleHookImplementations takes care of it I think so I've removed that.

    I also want to document the .theme discussion from slack here a bit clearer.

    From godotislate:

    you can implement hook_form_alter, hook_page_attachments_alter, among a few others in .theme
    but that is via theme manager, not module handler
    it looks like they are all invoked after module handler has its turn
    so keeping them OOS seems fine

  • keeping them OOS

    What does "OOS" stand for?

  • 🇺🇸United States nicxvan

    It's what godotislate said in slack, I assume it means Out Of Scope.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    Note the FORMID constant was just a discussion in the issue, it's not found in the code yet. The attribution parameter is called form_id.

    I marked the PR as non-draft.

  • Pipeline finished with Failed
    about 1 month ago
    Total: 184s
    #175478
  • 🇺🇸United States nicxvan

    I fixed the codesniff issues. I think that only runs once the MR is out of draft which is why we didn't catch it before.

  • Pipeline finished with Failed
    about 1 month ago
    Total: 879s
    #176696
  • 🇺🇸United States nicxvan

    I rebased to try to fix the unrelated failures.

  • Status changed to Needs work about 1 month ago
  • The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. 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.

  • Pipeline finished with Success
    about 1 month ago
    Total: 695s
    #176708
  • Status changed to Needs review about 1 month ago
  • 🇦🇺Australia acbramley

    naughty bot

  • 🇺🇸United States nicxvan

    Thanks! I was about to say the same lol, tests are passing again.

  • 🇺🇸United States nicxvan

    I addressed the two comments on the MR that I could, there are a few comments that @Ghost of Drupal Past should address from @solideogloria.

    Two questions about targeting classes: #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
    And the regex here can module names have pipes | in them: $module_preg = '/^function (?(?' . implode('|', $modules) . ')_(?!preprocess_)(?[a-zA-Z0-9_\x80-\xff]+))\(/m';

  • Pipeline finished with Success
    about 1 month ago
    Total: 625s
    #176718
  • Status changed to Needs work about 1 month ago
  • The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. 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.

  • Status changed to Needs review about 1 month ago
  • Pipeline finished with Success
    about 1 month ago
    Total: 671s
    #176962
  • Assigned to nicxvan
  • 🇺🇸United States nicxvan

    I'm rebasing

  • Issue was unassigned.
  • 🇺🇸United States nicxvan

    Ok I rebased this so it applies again.

    @solideogloria's questions have been responded to on the MR.

    Two remaining tasks are whether more tests are needed and a framework manager review.

  • Pipeline finished with Success
    27 days ago
    Total: 1368s
    #185223
  • 🇺🇸United States nicxvan

    Copying some relevant abridged discussion from slack if others think the class target is worth dropping:

    chx (he/him)
    if we dont want the class target, dropping it is really easy

    chx (he/him)
    10 hours ago
    one could write the relevant part of getHookAttributesInClass like this to show it doesn't leak

    <?
    $class_implementations = array_map(function (\ReflectionAttribute $reflection_attribute) use ($class) {
    $hook = $reflection_attribute->newInstance();
    assert($hook instanceof Hook);
    if (!$hook->method) {
    if (method_exists($class, '__invoke')) {
    $hook->setMethod('__invoke');
    }
    else {
    throw new \LogicException("The Hook attribute for hook $hook->hook on class $class must specify a method.");
    }
    }
    return $hook;
    }, $reflection_class->getAttributes(Hook::class, \ReflectionAttribute::IS_INSTANCEOF));

  • Pipeline finished with Success
    26 days ago
    Total: 724s
    #186223
  • 🇺🇸United States nicxvan

    @chx nor I can reproduce the config validation failures locally so any insight would be appreciated.

  • 🇨🇦Canada Charlie ChX Negyesi 🍁Canada

    I pushed a small change which shows there's no need to use loadInclude now. I am not 100% on doing this but it'll go away anyways when node admin is converted to a class which seems inevitable.

  • Pipeline finished with Success
    23 days ago
    Total: 727s
    #188482
Production build 0.69.0 2024