Skip to content

A hook is not a kind of event, so stop saying it is - #1232

Merged
mastacontrola merged 1 commit into
working-1.6from
hook-not-an-event
Aug 19, 2026
Merged

A hook is not a kind of event, so stop saying it is#1232
mastacontrola merged 1 commit into
working-1.6from
hook-not-an-event

Conversation

@mastacontrola

Copy link
Copy Markdown
Member

Closes #1203.

Hook extends Event made $listener instanceof Event — the only type check
separating the two — true for every hook in existence. #1194 closed the defect
that fell out of it with a refusal in acceptListener(); it did not fix the
modelling that made the wrong registration look reasonable.

The change

Hook extends FOGBase. The boilerplate the two genuinely share — $name,
$description, $active, the three log settings, log() and the constructor —
moves to a Listener trait that both use, so nothing a hook relied on went away
with the parent. What did go away is run() and onEvent(): both are the
event dispatch surface, both were empty bodies, and no hook in core or in
fog-plugins calls either.

log() had to move rather than stay on Event. FOGBase declares a log()
with the identical signature and a completely different job — it writes a
history row — so a hook that lost Event's copy would not have failed, it would
have quietly called that one.

acceptListener() now tests for Hook before Event. Both arms refuse a hook
once the inheritance is gone, and whichever runs first decides the message; "a
hook is not an event listener" is the one that tells a plugin author what they
did, where "Class must extend event" would send them off to add the very
extends this removes.

Blast radius, measured before the change

Question Answer
instanceof Event in packages/ 1 site, in eventmanager.class.php
instanceof Event in packages/service 0
instanceof Event in fog-plugins (72 hooks, 15 events) 0
hooks not declaring their own $active none, in either tree
hooks not calling parent::__construct() none, in either tree
files using $logLevel/$logToFile/$logToBrowser/log() 2, both core, both $active = false
hooks declaring or calling run()/onEvent() 1 declares its own run(); none call either

The residue no sweep can see is a plugin that type-hints Event on its own
function and passes its own hook to it. Nothing in core or fog-plugins does
this. Core never puts a hook in front of plugin code where that type is tested —
hook dispatch passes [$obj, $method] callables, and acceptListener() has
refused hooks outright since #1194 — so a plugin asking the question is either
mirroring core's pre-#1194 behaviour (already broken by #1194, not by this) or
running its own dispatch and calling onEvent() on a hook, which #1194 emptied.

Verification

Booted the app from a shadow tree against the live 1.6 database and constructed
every listener the managers load: 52 hooks, 6 events, 0 problems, each
checked for the four properties the parent used to supply and for log()
resolving to the trait rather than FOGBase. Driver:
scripts/background_scripts/boot_hooks_1203.php.

tests/hook-event-contract.test.php gains three guards, each mutation-verified —
reverting the extends fails the first, dropping use Listener fails the other
two. Its Event::onEvent() case now reflects on an event rather than a hook;
passing a hook was only possible while the inheritance stood, and the assertion
itself is unchanged. Full suite: 76 passed, 0 failed.

Not in scope

HookManager extends EventManager is the same shape one level up and is
deliberately untouched — docs/hook-event-plan.md §6 records why.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GN7hADN5QLzoxXWDA6xeUk

Hook extended Event, which made `$listener instanceof Event` -- the only
type check separating the two -- true for every hook in existence. #1194
closed the defect that fell out of it (a hook could register as an event
listener and be dispatched through notify()) with a refusal in
acceptListener(); it did not fix the modelling that made the wrong
registration look reasonable.

Hook now extends FOGBase. The boilerplate the two genuinely share --
$name, $description, $active, the three log settings, log() and the
constructor -- moved to a Listener trait that both use, so nothing a hook
relied on went away with the parent. What did go away is run() and
onEvent(): both are the event dispatch surface, both were empty bodies,
and no hook in core or in fog-plugins calls either.

log() had to move rather than stay on Event. FOGBase declares a log()
with the identical signature and a completely different job -- it writes
a history row -- so a hook that lost Event's copy would not have failed,
it would have quietly called that one. Two core hooks call self::log().

acceptListener() now tests for Hook before Event. Both arms refuse a hook
once the inheritance is gone, and whichever runs first decides the
message; "a hook is not an event listener" is the one that tells a plugin
author what they did, where "Class must extend event" would send them off
to add the very `extends` this removes.

Blast radius, measured before the change:

  grep -rn 'instanceof Event' packages/                 # 1, in eventmanager
  grep -rn 'instanceof Event' ~/fog-plugins             # 0 across 72 hooks
  grep -rLE 'parent::__construct' **/*.hook.php         # none
  grep -rl 'logLevel|logToFile|logToBrowser|::log('     # 2, both core,
                                                        # both $active=false

Verified by booting the app from a shadow tree against the live 1.6
database and constructing every listener the managers load: 52 hooks and
6 events, 0 problems, each checked for the four properties the parent
used to supply and for log() resolving to the trait rather than FOGBase.
The driver is scripts/background_scripts/boot_hooks_1203.php.

hook-event-contract.test.php gains three guards, each mutation-verified:
reverting the `extends` fails the first, dropping `use Listener` fails
the other two. Its Event::onEvent() case now reflects on an event rather
than a hook -- passing a hook was only possible while the inheritance
stood, and the assertion is unchanged.

Closes #1203.

Co-Authored-By: Claude <noreply@anthropic.com>
@mastacontrola
mastacontrola merged commit 131f032 into working-1.6 Aug 19, 2026
4 checks passed
@mastacontrola
mastacontrola deleted the hook-not-an-event branch August 19, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants