Summary
ExtensionJsonTestBase only verifies that each HookHandlers entry can be constructed; nothing checks that a handler registered under Hooks actually implements the hook it is registered for. As a result, a dangling registration — left behind when a handler method is removed but its Hooks entry is not — goes unnoticed by the extension.json structure tests and only surfaces at runtime when the hook fires.
Technical notes
Add a data-driven test (testHookImplementedByHandler) to tests/phpunit/integration/includes/ExtensionJsonTestBase.php, reusing the existing provideHookNames provider. For each handler that references a HookHandlers entry, construct the object via ObjectFactory (matching testHookHandler, which also covers factory-based specs) and assert it has the matching on<Hook> method, using the same hook-name normalization as HookContainer::getHookMethodName() ('on' . strtr($hook, ':\\-', '___')). For legacy Class::method callable handlers, assert the callable is valid via assertIsCallable. Keep this separate from testHookUsesHookHandler, which enforces the stricter "must use HookHandlers" policy behind $requireHookHandlers and rejects callable handlers outright — the two checks are complementary (style policy vs. correctness). The test runs for every extension that extends ExtensionJsonTestBase, so it applies without any per-extension opt-in flag.
Open questions
The new test overlaps with testHookHandler: both construct every referenced HookHandlers object via ObjectFactory. This introduces two forms of duplicity worth deciding on before merge. First, redundant construction — a handler registered for N hooks is now constructed N times here, on top of the once in testHookHandler; construction cost on the test path is negligible, so this is mostly a tidiness concern. Second, overlapping failure signal — if a handler's construction throws, both tests fail for the same root cause, which is harmless but noisier in CI output. Options: (a) keep the tests separate and accept the overlap, favouring a clean separation of concerns (construction validity vs. hook implementation); (b) fold the method check into testHookHandler, constructing each handler once, at the cost of a reverse handler→hooks lookup and mixing two concerns in one test; (c) drop ObjectFactory here and check method_exists statically on the declared class name, which removes all construction overhead but loses coverage of factory-based specs (the reason ObjectFactory was chosen).
Acceptance criteria
- Extension.json structure tests fail when a hook is registered to a handler that lacks the corresponding on<Hook> method (or an invalid callable), and pass otherwise.
- The check covers both HookHandlers-style registrations (including factory-based specs) and legacy Class::method callables.