Improve HookRegistry hook calling conventions #8089
Replies: 2 comments 17 replies
-
I like it! 👍 A few thoughts:
HookRegistry::register('my::hook', function(string $hookName, Submission $submission, $result = null) {
return true;
});
HookRegistry::register('my::hook', function(string $hookName, Submission $submission, $result = null) {
if ($result === true) {
return $result;
}
return false;
}); Not loving that syntax, but you get the idea. A bit like
We could use small static classes for hooks, which would give us code completion and type hinting in the editors. The downside is that we'd have a lot of small classes added to the codebase: use PKP\hook\Submission as HookSubmission;
HookRegistry::register(HookSubmission::add, function(Submission $submission) {});
HookRegistry::execute(HookSubmission::add($submission)); Or we could come up with a consistent docblock format. We would be able to generate documentation, but wouldn't get in-editor code completion: /**
* A submission has been added
*
* @param Submission $submission
*/
Hook::execute('Submission::add', [$submission]); |
Beta Was this translation helpful? Give feedback.
-
Just capturing some thoughts in case I decide to push them further. Now that we have first class callable syntax in PHP, we can already use callables for hook registrants. However, we may also consider using first-class callables for hook naming. Rather than registering and calling hooks as strings... // Adding a hook registrant
Hook::add('MyClass::myHookName', fn() => {...});
// Running a hook, usually done within the MyClass::myHookName function
Hook::run('MyClass::myHookName'); ...we may want to use callables interchangeably with strings: // Adding a hook registrant
Hook::add(MyClass::myHookName(...), fn() => {...});
// Running a hook, usually done within the MyClass::myHookName function
Hook::run($this->myHookName(...));
// ...or maybe...
Hook::run(__METHOD__); // not actually a first class callable, but also avoids repeating the method name Experimentally, this can be accomplished with the following: diff --git a/classes/plugins/Hook.php b/classes/plugins/Hook.php
index e425759b1b..4103875af2 100644
--- a/classes/plugins/Hook.php
+++ b/classes/plugins/Hook.php
@@ -31,18 +31,19 @@ class Hook
/**
* Get the current set of hook registrations.
*
- * @param string $hookName Name of hook to optionally return
+ * @param string $hook Hook to optionally return
*
- * @return mixed Array of all hooks or just those attached to $hookName, or
- * null if nothing has been attached to $hookName
+ * @return mixed Array of all hooks or just those attached to $hook, or
+ * null if nothing has been attached to $hook
*/
- public static function &getHooks(?string $hookName = null): ?array
+ public static function &getHooks(callable|string|null $hook = null): ?array
{
$hooks = & Registry::get('hooks', true, []);
- if ($hookName === null) {
+ if ($hook === null) {
return $hooks;
}
+ $hookName = self::possibleCallableToString($hook);
if (isset($hooks[$hookName])) {
return $hooks[$hookName];
}
@@ -52,23 +53,25 @@ class Hook
}
/**
- * Clear hooks registered against the given name.
+ * Clear registrants from the specified hook.
*/
- public static function clear(string $hookName): void
+ public static function clear(callable|string $hook): void
{
+ $hookName = self::possibleCallableToString($hook);
$hooks = & static::getHooks();
unset($hooks[$hookName]);
}
/**
- * Register a hook against the given hook name.
+ * Add a registrant against the given hook.
*
- * @param string $hookName Name of hook to register against
+ * @param string $hook Hook to register against
* @param callable $callback Callback pseudo-type
* @param int $hookSequence Optional hook sequence specifier SEQUENCE_...
*/
- public static function add(string $hookName, callable $callback, int $hookSequence = self::SEQUENCE_NORMAL): void
+ public static function add(callable|string $hook, callable $callback, int $hookSequence = self::SEQUENCE_NORMAL): void
{
+ $hookName = self::possibleCallableToString($hook);
$hooks = & static::getHooks();
$hooks[$hookName][$hookSequence][] = $callback;
}
@@ -114,20 +117,21 @@ class Hook
}
/**
- * Call each callback registered against $hookName in sequence.
+ * Call each callback registered against $hook in sequence.
* The first callback that returns ABORT will interrupt processing and
* this function will return ABORT; otherwise, all callbacks will be
* called in sequence and the return value of this call will be
* CONTINUE.
*
* The signature of a callback function should be:
- * function callback($hookName, ...) : bool;
+ * function callback($hook, ...) : bool;
* where ... corresponds to the parameters named/listed in the $args
* parameter to Hook::call. These may be named if desired,
* and may include references.
*/
- public static function run(string $hookName, $args): bool
+ public static function run(callable|string $hook, $args): bool
{
+ $hookName = self::possibleCallableToString($hook);
$hooks = & static::getHooks();
if (!isset($hooks[$hookName])) {
return self::CONTINUE;
@@ -137,7 +141,7 @@ class Hook
foreach ($hooks[$hookName] as $priority => $hookList) {
foreach ($hookList as $callback) {
$params = array_merge([$hookName], $args);
- if (call_user_func_array($callback, array_merge([$hookName], $args)) === self::ABORT) {
+ if (call_user_func_array($callback, array_merge([$hook], $args)) === self::ABORT) {
return self::ABORT;
}
}
@@ -195,6 +199,14 @@ class Hook
static $calledHooks = [];
return $calledHooks;
}
+
+ protected static function possibleCallableToString(callable|string|null $callableOrString): string
+ {
+ if (is_string($callableOrString)) return $callableOrString;
+
+ $rf = new \ReflectionFunction($callableOrString);
+ return get_class($rf->getClosureThis()) . '::' . $rf->getName();
+ }
}
if (!PKP_STRICT_MODE) { This might move us ahead in our quest for proper hook self-documentation and static analysis, and reduces the frequent repetition of class and function names when calling hooks that are often named identically to the current function. |
Beta Was this translation helpful? Give feedback.
-
HookRegistry
has some long-standing implementation quirks that make hook-based code hard to read and maintain:Clean up these conventions (in a backwards-compatible way).
Proposal PR: #8084
HookRegistry
class has been renamed toHook
, with backwards-compatible class aliases provided.Hook::add
is now preferred toHookRegistry::register
. Backwards compatible.Hook::run
is now preferred toHookRegistry::call
. Backwards compatible -- but to emulate old calling conventions using a parameter array, wrap the parameters in[]
a second time:Passing parameters to hook callbacks
Hook::run
offers several new possibilities for callbacks:Callback function parameters properly declared
Previously, callbacks had to pass parameters via an array:
Using
Hook::run
instead, the parameters can be named directly, including type hinting:References are also supported; these can be declared in the callback parameter list as usual, and should be passed into the array as references as well (as before, even with objects):
Named arguments
Hook::run
supports named arguments; this can help keep code readable when longer lists of parameters are provided or parameter ordering might be unclear.Hook return conventions
In the past, hook callbacks returned
true
to interrupt later hooks, orfalse
to continue. The meaning of these values was hard to remember. This change addsHook::ABORT
andHook::CONTINUE
constants to use instead. (This change is backwards compatible; the constants are equivalent totrue
andfalse
.)Beta Was this translation helpful? Give feedback.
All reactions