-
-
Notifications
You must be signed in to change notification settings - Fork 186
Improvements to hooks #2541
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
Improvements to hooks #2541
Conversation
|
This looks awesome! 🔥 |
|
I added the |
|
I think I need an actual example to really grasp what this is for... |
|
Currently we can manage the hooks in a limited way. To give the simplest example, we cannot use more than one instance from the same hook. With the Btw i'm thinking of a few new features such as when() return [
'hooks' => [
'page:before' => function ($hook) {
// page.create:before
$hook->when($condition, 'create', function ($page, $input) {
// your codes
});
}
]
];template() return [
'hooks' => [
'page:before' => function ($hook) {
// page.create:before
$hook->template('product', 'create', function ($page, $input) {
// your codes
});
}
]
];group() return [
'hooks' => [
'page:before' => function ($hook) {
$hook->group(function () {
$this
// page.create:before
->promise('create', function ($page, $input) {
// your codes
})
// page.create:before (same action can use multiple)
->promise('create', function ($page, $input) {
// your codes
})
// page.update:before
->promise('update', function ($page, $values, $strings) {
// your codes
})
// page.delete:before
->promise('delete', function ($page) {
// your codes
});
});
}
]
]; |
|
Looks all very sophisticated, but what exactly for? |
|
For example; In a simple e-commerce application, I need to write a code like this to follow the changes or relationships in the products and category pages. It doesn't seem useful to me at all. Indeed, only return [
'hooks' => [
'page.create:before' => function ($page, $input) {
if ($page->intendedTemplate() === 'product') {
// your code goes here
} elseif ($page->intendedTemplate() === 'category') {
// your code goes here
}
},
'page.create:after' => function ($page) {
if ($page->intendedTemplate() === 'product') {
// your code goes here
} elseif ($page->intendedTemplate() === 'category') {
// your code goes here
}
},
'page.update:before' => function ($page, $values, $strings) {
if ($page->intendedTemplate() === 'product') {
// your code goes here
} elseif ($page->intendedTemplate() === 'category') {
// your code goes here
}
},
'page.update:after' => function ($newPage, $oldPage) {
if ($newPage->intendedTemplate() === 'product') {
// your code goes here
} elseif ($newPage->intendedTemplate() === 'category') {
// your code goes here
}
},
'page.duplicate:before' => function ($originalPage, $input, $options) {
if ($originalPage->intendedTemplate() === 'product') {
// your code goes here
} elseif ($originalPage->intendedTemplate() === 'category') {
// your code goes here
}
},
'page.duplicate:after' => function ($duplicatePage) {
if ($duplicatePage->intendedTemplate() === 'product') {
// your code goes here
} elseif ($duplicatePage->intendedTemplate() === 'category') {
// your code goes here
}
},
'page.delete:before' => function ($page) {
if ($page->intendedTemplate() === 'product') {
// your code goes here
} elseif ($page->intendedTemplate() === 'category') {
// your code goes here
}
},
'page.delete:after' => function ($status, $page) {
if ($page->intendedTemplate() === 'product') {
// your code goes here
} elseif ($page->intendedTemplate() === 'category') {
// your code goes here
}
}
]
]; |
|
@distantnative Here's a sample of code that motivated my original feature request. Kirby::plugin('thestreamable/cacheclearqueue', [
'hooks' => [
// Page
'page.changeNum:after' => function (Page $page, Page $oldPage) {
$page->queueCacheInvalidation('page.changeNum:after', $oldPage);
},
'page.changeSlug:after' => function (Page $page, Page $oldPage) {
if ($page->isPublished()) {
$page->queueCacheInvalidation('page.changeSlug:after', $oldPage);
}
},
'page.changeStatus:after' => function (Page $page, Page $oldPage) {
$page->queueCacheInvalidation('page.changeStatus:after', $oldPage);
},
'page.changeTemplate:after' => function (Page $page, Page $oldPage) {
if ($page->isPublished()) {
$page->queueCacheInvalidation('page.changeTemplate:after', $oldPage);
}
},
'page.changeTitle:after' => function (Page $page, Page $oldPage) {
if ($page->isPublished()) {
$page->queueCacheInvalidation('page.changeTitle:after', $oldPage);
}
},
'page.create:after' => function (Page $page) {
if ($page->isPublished()) {
$page->queueCacheInvalidation('page.create:after');
}
},
'page.delete:after' => function (bool $force, Page $oldPage) {
if ($oldPage->isPublished()) {
$oldPage->queueCacheInvalidation('page.delete:after');
}
},
'page.duplicate:after' => function (Page $page) {
if ($page->isPublished()) {
$page->queueCacheInvalidation('page.duplicate:after');
}
},
'page.update:after' => function (Page $page, Page $oldPage) {
if ($page->isPublished()) {
$page->queueCacheInvalidation('page.update:after', $oldPage);
}
},
// File
'file.changeName:after' => function (File $file, File $oldFile) {
$file->queueCacheInvalidation('file.changeName:after', $oldFile);
},
'file.changeSort:after' => function (File $file, File $oldFile) {
$file->queueCacheInvalidation('file.changeSort:after', $oldFile);
},
'file.create:after' => function (File $file) {
$file->queueCacheInvalidation('file.create:after');
},
'file.delete:after' => function (bool $force, File $oldFile) {
$oldFile->queueCacheInvalidation('file.delete:after');
},
'file.replace:after' => function (File $file, File $oldFile) {
$file->queueCacheInvalidation('file.replace:after', $oldFile);
},
'file.update:after' => function (File $file, File $oldFile) {
$file->queueCacheInvalidation('file.update:after', $oldFile);
},
// Site
'site.update:after' => function (Site $site, Site $oldSite) {
$site->queueCacheInvalidation('site.update:after', $oldSite);
},
],
]);With this PR, it could be condensed down to something resembling: Kirby::plugin('thestreamable/cacheclearqueue', [
'hooks' => [
'page:after' => function ($hook) {
if ($hook->page()->isPublished() || !in_array($hook->action(), [
'changeSlug',
'changeTemplate',
'changeTitle',
'create',
'delete',
'duplicate',
'update',
]) {
$hook->page()->queueCacheInvalidation($hook->name(), $hook->oldPage());
}
},
'file:after' => function ($hook) {
$hook->file()->queueCacheInvalidation($hook-name(), $hook->oldFile());
},
'site:after' => function ($hook) {
$hook->site()->queueCacheInvalidation($hook-name(), $hook->oldSite());
},
],
]);It's less code, yes, but the more interesting aspect is that: as new hooks are added, this code will automatically be involved in those hooks. If there's a new |
|
Admittedly though, I don't find the Considering that something like Personally, I find this: Kirby::plugin('thestreamable/testplugin', [
'hooks' => [
'page:after' => function ($hook) {
$hook->promise('create', function ($page) {
// do something
});
},
],
]);much less clear than simply doing: Kirby::plugin('thestreamable/testplugin', [
'hooks' => [
'page:after' => function ($hook) {
if ($hook->action() == 'create') {
// do something
}
},
],
]);And, in a "single" use case like that, I think the existing syntax preferable. Kirby::plugin('thestreamable/testplugin', [
'hooks' => [
'page.create:after' => function ($hook) {
// do something
},
],
]); |
lukasbestle
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Neil that the current implementation is a bit too complex and does too much. I think we should keep this as simple as possible for now to make it easier to understand. Which means that the Hook should probably only have the methods it absolutely needs.
Regarding implementation:
- Now that I think about it, I'm not sure about the different arguments for "old-style" hooks and the new global hooks. What we could do is to use
ReflectionFunctionlike in theToolkit\Controllerclass to determine at run-time which arguments each individual hook would like to receive. We could even support arbitrary argument orders that way and it would also be possible to receive$hookin addition to some or all of the individual arguments. - Triggering both the individual as well as the "global" hooks should be automated in the
$app->trigger()method. So$app->trigger()would ideally only take aHookobject and then determine the hooks to call from that. - I'm not sure how we can make this feature work with
$app->apply(). To make it all consistent, everything we change about$app->trigger()should also be changed for$app->apply(). But that's going to be a bit difficult.
I do not fully agree with you about like My personal opinion is; It just will help developers by adding flexibility. |
f39a804 to
d18fe4b
Compare
|
In case you are wondering: I've rebased this PR against I've taken a look at your updated implementation. A bit of feedback:
@afbora Do you want to continue with the implementation or do you want me to take over? Either way is fine for me. Regarding the helper methods: I see your point, but the issue with closures is that you need to manually pass through all variables to them with However we can reconsider this later, but only after this PR is merged. This PR is going to be quite complex in itself and the more features we can keep out of it for later, the better we'll be able to keep an overview. I hope you understand. |
|
Nope. :) |
cbad0dc to
3bdfffb
Compare
Allows for arbitrary argument orders in hook functions as well as for a new optional `$event` argument for every hook. NOTE: Breaking-change as the method signature of $app->trigger() and $app->apply() has changed. Implements getkirby/ideas#194.
3bdfffb to
617cbdf
Compare
|
This PR is now ready for review. I have also updated the first post of this PR with the details on what exactly has changed, how to use it and which changes are breaking-changes. I'm really looking forward to your feedback! In my opinion the named arguments, the Thanks a lot to @afbora for the initial inspiration. My implementation works quite differently than the original draft by @afbora, but the draft included a lot of really awesome ideas I could use in the final implementation. ❤️👍😘 |
|
To ease the migration to 3.4, I have created a hook migration checker plugin that we can include as a release attachment on GitHub to automate checking the hook arguments for sites and plugins: And once the issues are fixed: |
|
If we are breaking hooks anyways... could we incorporate this somehow? #2425 |
|
@distantnative Oh, absolutely. It should actually be very easy to implement now as we no longer need to worry about argument order. I can take a look once this PR is merged. |
|
@lukasbestle I always love your committees ❤️ You thought of everything 🚀 There is nothing left from my initial PR 😄 I haven't seen any issue or missing in a few tests at my local. Everything looks perfect. I'm looking forward to the ideas waiting in your draft 👀 |
Co-Authored-By: Ahmet Bora <[email protected]>
|
The wildcards are fantastic and it's awesome that you created the migration tool, @lukasbestle!! |


Describe the PR
General note: As this is a large PR, reviewing this from the "Commits" view instead of from the "Files changed" view is recommended to keep the overview. I was very careful to split the PR up into useful commits that make sense on their own, which should help with reviewing in this way. Splitting this PR up into separate PRs was unfortunately not possible as we would then have needed three PRs at minimum, which would be even harder to keep an overview on.
null.page.*:beforeorpage.create:*or*.create:*– all variations are supported!) that will be triggered for all event actions of the given type.Note: The support for all wildcard variations is included in the fifth commit. If we decide that a simple
page.*:beforehook without the other variations is enough (which wouldn't be as flexible though), we can simply remove the fifth commit.$eventobject provided as argument for all hooks that contains all information about the triggered event. The$eventargument can be used instead of or in addition to the individual hook arguments.Breaking-changes:
$app->apply()and$app->trigger()has changed. Instead of a list of arguments, they now receive an associative array with named arguments.$app->apply()now also requires a third argument that tells the method which argument should be modified by the hooks. As both methods are marked for internal use and also not very useful in sites, the only use-case for them so far was hooks defined in plugins. Plugin authors who depend on any of the two changed methods need to update their plugins to support the new signature.nullfor those arguments instead. I expect that most users already use the naming from the reference as the reference provides copy-paste code snippets. However all hooks should be checked for wrong variable naming. Sometimes the issue will be obvious as the hook will just not work, but it's still better to check beforehand. I have developed a migration checker plugin for this.Related issues
page.*:after) ideas#194Ready?
composer fix