Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RTM] Allow to register hooks listeners as tagged services #1094

Merged
merged 14 commits into from
Oct 26, 2017

Conversation

dmolineus
Copy link
Contributor

@dmolineus dmolineus commented Sep 20, 2017

Since #376 it's possible to use services for hooks. This is a great step to using services within Contao.

This PR completes this step by allowing to register hooks as services without using the config.php. You don't have to manage your hook registration at two places anymore.

services:
  my.custom.hook_listener:
    class: 'My\Custom\Hook\Listener'
    tags:
      - { name: 'contao.hook', hook: 'parseTemplate', method: 'onParseTemplate'}
      - { name: 'contao.hook', hook: 'initializeSystem', method: 'onInitializeSystem', before: true}

I'm aware of the above mentioned PR discussed it before and this feature wasn't added because of lack of control the position of the hook.

To gain more control of the position it's possible to set the before attribute so that the hook is registered before the legacy hooks. If you really need more control, well, just use the globals.

To keep backward compatibility the registered hooks are added to the $GLOBALS['TL_HOOKS'] array.

@aschempp
Copy link
Member

I very much like this idea! Even thought we agreed not to "improve" the "old" stuff, this will be helpful a lot. However, I would suggest two changes:

  1. Use priorities for the hooks. We can simply take the existing hook array keys as their internal priority (reversed). This way we will follow Symfony standards.
  2. The setup handling should not be done in the framework class, but using an event listener on the initializeSystem hook (which obviously needs to be registered using the old style ^^). Unfortunately, that would mean initializeSystem hooks are not taggable, but I can't see any other good solution.

@leofeyer leofeyer added this to the 4.5.0 milestone Sep 20, 2017
@leofeyer leofeyer changed the title Allow to register hooks listeners as tagged services [WIP] Allow to register hooks listeners as tagged services Sep 20, 2017
@leofeyer
Copy link
Member

Use priorities for the hooks.

+1

The setup handling should not be done in the framework class

Since it is part of initializing the Contao 3 framework, it would be ok for me (not ideal of course).

@dmolineus
Copy link
Contributor Author

dmolineus commented Sep 20, 2017

Use priorities for the hooks. We can simply take the existing hook array keys as their internal priority (reversed). This way we will follow Symfony standards.

I thought about it, but wasn't sure how to handle the legacy hooks. To confirm if I got it right:

Given are registered hooks in $GLOBALS['TL_HOOKS'] and I want to add my hook at a specific position:

<?php
$GLOBALS['TL_HOOKS']['parseTemplate'] = [
  ['Foo', 'onParseTemplate'],
  ['Bar', 'onParseTemplate'],
  ['Baz', 'onParseTemplate'],
];

So your suggestion is, that the Foo listener is handled as priority 2, Bar as priority 1 and Baz ans priority 0. To if I want to be at first position I have to use priority 4. Did I understand you correctly?

What I dislike here that you never know how many hooks are registered. Symfony standard doesn't depend on the number of listeners as well. I suggest handle all legacy hooks as priority 0 instead.

@Toflar
Copy link
Member

Toflar commented Sep 20, 2017

Cool, I like the idea, well done @dmolineus!

@aschempp
Copy link
Member

I would agree with the priority 0, but I see issues here. The legacy order is important, because extensions are loaded after each other and might depend on it. So we need to make sure the legacy order stays the same. The most simple solution will be to array_reverse the old hooks and then use the keys as priorities.

@aschempp
Copy link
Member

Regarding initialization in the framework class, let's just make sure we agree on this once and for all, because @Toflar used the hook in the new module as controller pull request to merge legacy with new stuff. Everything should go into the framework class then, and that'll make it huge…

@dmolineus
Copy link
Contributor Author

I would agree with the priority 0, but I see issues here. The legacy order is important, because extensions are loaded after each other and might depend on it. So we need to make sure the legacy order stays the same. The most simple solution will be to array_reverse the old hooks and then use the keys as priorities.

I would solve it this way:

  • Priority > 0 will be added before the legacy hooks depending on the priority
  • Now the legacy hooks are added in the ordered they are added
  • Priority = 0 comes after the legacy hooks
  • Priority < 0 comes after that

Another solution could be to to load the legacy hooks and hooks with priority null for each bundle at the same time. So the order would be:

  • Priority > 0 will be added before the legacy hooks depending on the priority
  • Bundle A legacy hooks
  • Bundle A hook listeners with priority 0
  • Bundle B legacy hooks
  • Bundle B hook listeners with priority 0
  • ...
  • Priority < 0 comes after that

@dmolineus
Copy link
Contributor Author

Regarding initialization in the framework class, let's just make sure we agree on this once and for all, because @Toflar used the hook in the new module as controller pull request to merge legacy with new stuff. Everything should go into the framework class then, and that'll make it huge…

Maybe adding an event which is triggered before the initialization hook would allow to handle such tasks as the module as controller feature or the hook listener registration.

@Toflar
Copy link
Member

Toflar commented Sep 21, 2017

let's just make sure we agree on this once and for all

Agree on what?

@leofeyer
Copy link
Member

Regarding initialization in the framework class, let's just make sure we agree on this once and for all, because @Toflar used the hook in the new module as controller pull request to merge legacy with new stuff. Everything should go into the framework class then, and that'll make it huge…

He means "agree on not putting everything into the framework class and use listeners/hooks instead". Although this is a special case IMO, because of

Unfortunately, that would mean initializeSystem hooks are not taggable, but I can't see any other good solution.

@aschempp
Copy link
Member

I would still prefer to call an explicit event listener manually instead of having everything in the framework class…

Regarding the priority 0, I'm not sure if the order of hooks inside one priority is kept when sorting them by priority?

@dmolineus
Copy link
Contributor Author

I reworked the hook registration using the priority attribute. I've chosen this strategy.

Priority > 0 will be added before the legacy hooks depending on the priority
Now the legacy hooks are added in the ordered they are added
Priority = 0 comes after the legacy hooks
Priority < 0 comes after that

Hooks with same priority will keep their internal order.

foreach ($tags as $attributes) {
$this->guardRequiredAttributesExist($serviceId, $attributes);

$priority = $attributes['priority'] ?? 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably int-cast this for safety reasons.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ec49f63.


if (count($hooks) > 0) {
// Apply priority sorting.
krsort($hooks);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure this works? Because it sorts on the key of the hook name, not the priority? Also, you might want to use the \SplPriorityQueue which is exactly meant for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I was because I craeted unit tests for it. But I had a closer look. It didn't work. assertEquals does not detect order of keys which I thought it would (and which I was used to do by PHPSpec which I actually prefer).

Fixed it in 802350f. Just used simple arrays as Symfony does the same in the event dispatcher.

// Apply priority sorting.
krsort($hooks);

$container->setParameter('contao.hook_listeners', $hooks);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hooks should not be registered as a parameter that is always available, but we should use dependency injection for it. That means something like $container->getDefinition('contao.framework')->setArgument(6, $hooks).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 728799e.

foreach ($config as $hookName => $priorities) {
$hooks = [];

foreach ($priorities as $priority => $listeners) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be simplified to something like this:

if (isset($GLOBALS['TL_HOOKS'][$hookName])) {
    if (isset($priorities[0]) {
        $priorities[0] = array_merge($GLOBALS['TL_HOOKS'][$hookName], $priorities[0]);
    } else {
        $priorities[0] = $GLOBALS['TL_HOOKS'][$hookName];
    }
}

$GLOBALS['TL_HOOKS'][$hookName] = array_merge_recursive($priorities);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because array_merge_recursive would merge like this:

$arrayA = ['foo','bar'];
$arrayB = ['baz'];

// would create [['foo', 'baz'], 'bar]
$merged = array_merge_recursive($arrayA, $arrayB);

@dmolineus dmolineus changed the title [WIP] Allow to register hooks listeners as tagged services [RTM] Allow to register hooks listeners as tagged services Oct 5, 2017
@Toflar
Copy link
Member

Toflar commented Oct 17, 2017

I would have a use case just now again :) Looking forward to this!

@aschempp
Copy link
Member

I just looked into this again. Came up with the idea whether it might be clever to actually use a Symfony EventDispatcher for hooks? Meaning that for every hook service in the container, we register a closure that calls the eventDispatcher with a GenericEvent ?

This way we could actually migrate from our hooks to real events. The only disadvantage would be that we can't have priorities mixed with the existing ones. Legacy hooks always come before or after the event dispatcher.

@dmolineus
Copy link
Contributor Author

I just looked into this again. Came up with the idea whether it might be clever to actually use a Symfony EventDispatcher for hooks? Meaning that for every hook service in the container, we register a closure that calls the eventDispatcher with a GenericEvent ?

What would be the benefit of using a GenericEvent instead of named events for it as prepared in #204? (Ingoring the initial work which has to be done to create it).

@Toflar
Copy link
Member

Toflar commented Oct 18, 2017

The only disadvantage would be that we can't have priorities mixed with the existing ones. Legacy hooks always come before or after the event dispatcher.

That's a no go because that's exactly what I would use this PR for.

@aschempp
Copy link
Member

aschempp commented Oct 18, 2017

What would be the benefit of using a GenericEvent instead of named events for it as prepared in #204? (Ingoring the initial work which has to be done to create it).

The difference to me is that you don't need to add event dispatcher code everywhere that hooks are already executed.

But I agree with @Toflar that priority settings is somewhat more important.

*/
private function buildHookGlobals(): void
{
foreach ($this->hookListeners as $hookName => $priorities) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I already mentioned that somewhere, but can't find it. These lines can be much simplified like this:

foreach ($this->hookListeners as $hookName => $priorities) {
    if (isset($GLOBALS['TL_HOOKS'][$hookName]) && is_array($GLOBALS['TL_HOOKS'][$hookName])) {
        if (isset($priorities[0]) {
            $priorities[0] = array_merge($GLOBALS['TL_HOOKS'][$hookName], $priorities[0]);
        } else {
            $priorities[0] = $GLOBALS['TL_HOOKS'][$hookName];
        }
    }

    $GLOBALS['TL_HOOKS'][$hookName] = array_merge_recursive($priorities);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You did, but it's still not true. array_merge_recursive with one argument will do nothing. Or do you mean array_merge_recursive(...$priorities)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, the correct call would be call_user_func_array('array_merge', $priorities)

Copy link
Contributor Author

@dmolineus dmolineus Oct 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I added your suggest change in 863f813. Think it's not really simplier, but I don't mind.

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* Find hook services and store them in an parameter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

@leofeyer leofeyer merged commit e700e19 into contao:develop Oct 26, 2017
@dmolineus dmolineus deleted the feature/tagged-hook-services branch October 26, 2017 13:48
@Toflar
Copy link
Member

Toflar commented Oct 26, 2017

So cool, thank you @dmolineus !

@aschempp
Copy link
Member

aschempp commented Nov 1, 2017

❤️

leofeyer added a commit that referenced this pull request Dec 13, 2019
Description
-----------

-

Commits
-------

09204b73 Upgrade to PHPStan 0.12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants