From ebc92b058ccf0c52aa51b2c967bbd898d6490c28 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Tue, 17 Feb 2026 11:19:58 +0100 Subject: [PATCH 1/4] Introduce `\Icinga\Application\Hook\Essentials` --- .../Icinga/Application/Hook/Essentials.php | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 library/Icinga/Application/Hook/Essentials.php diff --git a/library/Icinga/Application/Hook/Essentials.php b/library/Icinga/Application/Hook/Essentials.php new file mode 100644 index 0000000000..deabc86729 --- /dev/null +++ b/library/Icinga/Application/Hook/Essentials.php @@ -0,0 +1,59 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Application\Hook; + +use Icinga\Application\Hook; + +/** + * All hooks are provided and consumed - this trait provides the mechanism + */ +trait Essentials +{ + /** + * Get the name for {@link Hook::register} and {@link Hook::all} + */ + abstract protected static function getHookName(): string; + + /** + * Get whether the hook is registered + * + * @return bool + */ + public static function isRegistered(): bool + { + return Hook::has(static::getHookName()); + } + + /** + * Get all instances of the hook + * + * @return static[] + */ + public static function all(): array + { + return Hook::all(static::getHookName()); + } + + /** + * Get the first hook if any + * + * @return ?static + */ + public static function first(): ?static + { + return Hook::first(static::getHookName()); + } + + /** + * Register a hook provider + * + * @param bool $alwaysRun Whether to always run the hook, without permission check + */ + public static function register(bool $alwaysRun = false): void + { + Hook::register(static::getHookName(), static::class, static::class, $alwaysRun); + } +} From 9fa78a115434307acac818b7b324359c18dbd46a Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Thu, 26 Mar 2026 15:22:41 +0100 Subject: [PATCH 2/4] Use `\Icinga\Application\Hook\Essentials` in existing hooks --- .../Application/Hook/ApplicationStateHook.php | 7 ++++++ library/Icinga/Application/Hook/AuditHook.php | 7 ++++++ .../Application/Hook/AuthenticationHook.php | 7 ++++++ .../Application/Hook/ConfigFormEventsHook.php | 7 ++++++ .../Application/Hook/DbMigrationHook.php | 6 +++++ .../Icinga/Application/Hook/GrapherHook.php | 7 ++++++ .../Icinga/Application/Hook/HealthHook.php | 7 ++++++ .../Application/Hook/LoginButtonHook.php | 25 +++++++++---------- .../Icinga/Application/Hook/PdfexportHook.php | 7 ++++++ .../Icinga/Application/Hook/RequestHook.php | 21 ++++++++-------- .../Application/Hook/ThemeLoaderHook.php | 7 ++++++ .../Icinga/Application/Hook/TicketHook.php | 7 ++++++ 12 files changed, 91 insertions(+), 24 deletions(-) diff --git a/library/Icinga/Application/Hook/ApplicationStateHook.php b/library/Icinga/Application/Hook/ApplicationStateHook.php index afa061d532..b25b32ed7d 100644 --- a/library/Icinga/Application/Hook/ApplicationStateHook.php +++ b/library/Icinga/Application/Hook/ApplicationStateHook.php @@ -13,10 +13,17 @@ */ abstract class ApplicationStateHook { + use Essentials; + const ERROR = 'error'; private $messages = []; + protected static function getHookName(): string + { + return 'ApplicationState'; + } + final public function hasMessages() { return ! empty($this->messages); diff --git a/library/Icinga/Application/Hook/AuditHook.php b/library/Icinga/Application/Hook/AuditHook.php index 6a019c2a03..a338657663 100644 --- a/library/Icinga/Application/Hook/AuditHook.php +++ b/library/Icinga/Application/Hook/AuditHook.php @@ -13,6 +13,13 @@ abstract class AuditHook { + use Essentials; + + protected static function getHookName(): string + { + return 'audit'; + } + /** * Log an activity to the audit log * diff --git a/library/Icinga/Application/Hook/AuthenticationHook.php b/library/Icinga/Application/Hook/AuthenticationHook.php index 553e792b30..54b2fc2f01 100644 --- a/library/Icinga/Application/Hook/AuthenticationHook.php +++ b/library/Icinga/Application/Hook/AuthenticationHook.php @@ -18,11 +18,18 @@ */ abstract class AuthenticationHook { + use Essentials; + /** * Name of the hook */ const NAME = 'authentication'; + protected static function getHookName(): string + { + return static::NAME; + } + /** * Triggered after login in Icinga Web and when calling login action * diff --git a/library/Icinga/Application/Hook/ConfigFormEventsHook.php b/library/Icinga/Application/Hook/ConfigFormEventsHook.php index 6e83742c3a..0e9f9bcbea 100644 --- a/library/Icinga/Application/Hook/ConfigFormEventsHook.php +++ b/library/Icinga/Application/Hook/ConfigFormEventsHook.php @@ -15,9 +15,16 @@ */ abstract class ConfigFormEventsHook { + use Essentials; + /** @var array Array of errors found while processing the form event hooks */ private static $lastErrors = []; + protected static function getHookName(): string + { + return 'ConfigFormEvents'; + } + /** * Get whether the hook applies to the given config form * diff --git a/library/Icinga/Application/Hook/DbMigrationHook.php b/library/Icinga/Application/Hook/DbMigrationHook.php index fead618588..e8e0ab4f00 100644 --- a/library/Icinga/Application/Hook/DbMigrationHook.php +++ b/library/Icinga/Application/Hook/DbMigrationHook.php @@ -35,6 +35,7 @@ */ abstract class DbMigrationHook implements Countable { + use Essentials; use Translation; public const MYSQL_UPGRADE_DIR = 'schema/mysql-upgrades'; @@ -55,6 +56,11 @@ abstract class DbMigrationHook implements Countable /** @var ?string The current version of this hook */ protected $version; + protected static function getHookName(): string + { + return 'DbMigration'; + } + /** * Get whether the specified table exists in the given database * diff --git a/library/Icinga/Application/Hook/GrapherHook.php b/library/Icinga/Application/Hook/GrapherHook.php index 2f9abcac43..43aa3e3713 100644 --- a/library/Icinga/Application/Hook/GrapherHook.php +++ b/library/Icinga/Application/Hook/GrapherHook.php @@ -16,6 +16,8 @@ */ abstract class GrapherHook extends WebBaseHook { + use Essentials; + /** * Whether this grapher provides previews * @@ -30,6 +32,11 @@ abstract class GrapherHook extends WebBaseHook */ protected $hasTinyPreviews = false; + protected static function getHookName(): string + { + return 'grapher'; + } + /** * Constructor must live without arguments right now * diff --git a/library/Icinga/Application/Hook/HealthHook.php b/library/Icinga/Application/Hook/HealthHook.php index 6c0ade7668..1b775fcc40 100644 --- a/library/Icinga/Application/Hook/HealthHook.php +++ b/library/Icinga/Application/Hook/HealthHook.php @@ -15,6 +15,8 @@ abstract class HealthHook { + use Essentials; + /** @var int */ const STATE_OK = 0; @@ -39,6 +41,11 @@ abstract class HealthHook /** @var Url Url to a graphical representation of the available metrics */ protected $url; + protected static function getHookName(): string + { + return 'health'; + } + /** * Get overall state * diff --git a/library/Icinga/Application/Hook/LoginButtonHook.php b/library/Icinga/Application/Hook/LoginButtonHook.php index fc845834b3..8cec5b80c1 100644 --- a/library/Icinga/Application/Hook/LoginButtonHook.php +++ b/library/Icinga/Application/Hook/LoginButtonHook.php @@ -17,6 +17,15 @@ */ abstract class LoginButtonHook { + use Essentials { + register as protected parentRegister; + } + + protected static function getHookName(): string + { + return 'LoginButton'; + } + /** * Get the buttons to display below the login form * @@ -28,22 +37,12 @@ abstract class LoginButtonHook abstract public function getButtons(): array; /** - * Get all registered implementations - * - * @return static[] - */ - public static function all(): array - { - return Hook::all('LoginButton'); - } - - /** - * Register the class as a LoginButton hook implementation + * Register a hook provider * - * Call this method on your implementation during module initialization to make Icinga Web aware of your hook. + * Always runs the hook, without permission check. Latter makes no sense on the login page. */ public static function register(): void { - Hook::register('LoginButton', static::class, static::class, true); + static::parentRegister(true); } } diff --git a/library/Icinga/Application/Hook/PdfexportHook.php b/library/Icinga/Application/Hook/PdfexportHook.php index 2c1d6b3d4e..862cfa2edb 100644 --- a/library/Icinga/Application/Hook/PdfexportHook.php +++ b/library/Icinga/Application/Hook/PdfexportHook.php @@ -10,6 +10,13 @@ */ abstract class PdfexportHook { + use Essentials; + + protected static function getHookName(): string + { + return 'Pdfexport'; + } + /** * Get whether PDF export is supported * diff --git a/library/Icinga/Application/Hook/RequestHook.php b/library/Icinga/Application/Hook/RequestHook.php index 2f14255576..9af461ba5a 100644 --- a/library/Icinga/Application/Hook/RequestHook.php +++ b/library/Icinga/Application/Hook/RequestHook.php @@ -12,6 +12,15 @@ abstract class RequestHook { + use Essentials { + register as protected parentRegister; + } + + protected static function getHookName(): string + { + return 'RequestHook'; + } + /** * Triggered after a request has been dispatched * @@ -39,16 +48,6 @@ final public static function postDispatch(Request $request): void } } - /** - * Get all registered implementations - * - * @return static[] - */ - public static function all(): array - { - return Hook::all('RequestHook'); - } - /** * Register the class as a RequestHook implementation * @@ -56,6 +55,6 @@ public static function all(): array */ public static function register(): void { - Hook::register('RequestHook', static::class, static::class, true); + static::parentRegister(true); } } diff --git a/library/Icinga/Application/Hook/ThemeLoaderHook.php b/library/Icinga/Application/Hook/ThemeLoaderHook.php index cfd2bce92a..b0c6788979 100644 --- a/library/Icinga/Application/Hook/ThemeLoaderHook.php +++ b/library/Icinga/Application/Hook/ThemeLoaderHook.php @@ -13,6 +13,13 @@ */ abstract class ThemeLoaderHook { + use Essentials; + + protected static function getHookName(): string + { + return 'ThemeLoader'; + } + /** * Get the path for the given theme * diff --git a/library/Icinga/Application/Hook/TicketHook.php b/library/Icinga/Application/Hook/TicketHook.php index 7aac600b65..38af7d51d8 100644 --- a/library/Icinga/Application/Hook/TicketHook.php +++ b/library/Icinga/Application/Hook/TicketHook.php @@ -19,6 +19,8 @@ */ abstract class TicketHook { + use Essentials; + /** * Last error, if any * @@ -26,6 +28,11 @@ abstract class TicketHook */ protected $lastError; + protected static function getHookName(): string + { + return 'ticket'; + } + /** * Create a new ticket hook * From 8533c5eea28f310b2ddcde541594f1324ea7127f Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Tue, 17 Feb 2026 11:25:45 +0100 Subject: [PATCH 3/4] Use `Hook\Essentials::all()`, not `Hook::all()` to ease finding usages and to be a good example for future hooks. Same with `register()`, `first()` and `has()`. --- library/Icinga/Application/ApplicationBootstrap.php | 2 +- .../Icinga/Application/Hook/ApplicationStateHook.php | 6 ++---- library/Icinga/Application/Hook/AuditHook.php | 5 ++--- library/Icinga/Application/Hook/AuthenticationHook.php | 10 +++------- .../Icinga/Application/Hook/ConfigFormEventsHook.php | 5 ++--- library/Icinga/Application/Hook/HealthHook.php | 5 +---- library/Icinga/Application/MigrationManager.php | 3 +-- library/Icinga/File/Pdf.php | 3 ++- library/Icinga/Web/StyleSheet.php | 3 ++- .../Icinga/Web/Widget/Tabextension/OutputFormat.php | 3 ++- 10 files changed, 18 insertions(+), 27 deletions(-) diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php index cd6ae4f574..6ba8730528 100644 --- a/library/Icinga/Application/ApplicationBootstrap.php +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -741,7 +741,7 @@ public function hasLocales() */ protected function registerApplicationHooks(): self { - Hook::register('DbMigration', DbMigration::class, DbMigration::class); + DbMigration::register(); return $this; } diff --git a/library/Icinga/Application/Hook/ApplicationStateHook.php b/library/Icinga/Application/Hook/ApplicationStateHook.php index b25b32ed7d..5c89d8b5df 100644 --- a/library/Icinga/Application/Hook/ApplicationStateHook.php +++ b/library/Icinga/Application/Hook/ApplicationStateHook.php @@ -5,7 +5,6 @@ namespace Icinga\Application\Hook; -use Icinga\Application\Hook; use Icinga\Application\Logger; /** @@ -73,12 +72,11 @@ final public static function getAllMessages() { $messages = []; - if (! Hook::has('ApplicationState')) { + if (! static::isRegistered()) { return $messages; } - foreach (Hook::all('ApplicationState') as $hook) { - /** @var self $hook */ + foreach (static::all() as $hook) { try { $hook->collectMessages(); } catch (\Exception $e) { diff --git a/library/Icinga/Application/Hook/AuditHook.php b/library/Icinga/Application/Hook/AuditHook.php index a338657663..8243fae93f 100644 --- a/library/Icinga/Application/Hook/AuditHook.php +++ b/library/Icinga/Application/Hook/AuditHook.php @@ -34,7 +34,7 @@ protected static function getHookName(): string */ public static function logActivity($type, $message, ?array $data = null, $identity = null, $time = null) { - if (! Hook::has('audit')) { + if (! static::isRegistered()) { return; } @@ -46,8 +46,7 @@ public static function logActivity($type, $message, ?array $data = null, $identi $time = time(); } - foreach (Hook::all('audit') as $hook) { - /** @var self $hook */ + foreach (static::all() as $hook) { try { $formattedMessage = $message; if ($data !== null) { diff --git a/library/Icinga/Application/Hook/AuthenticationHook.php b/library/Icinga/Application/Hook/AuthenticationHook.php index 54b2fc2f01..b1f5747dd0 100644 --- a/library/Icinga/Application/Hook/AuthenticationHook.php +++ b/library/Icinga/Application/Hook/AuthenticationHook.php @@ -6,7 +6,6 @@ namespace Icinga\Application\Hook; use Icinga\User; -use Icinga\Web\Hook; use Icinga\Application\Logger; use Throwable; @@ -64,8 +63,7 @@ public function onLogout(User $user) */ public static function triggerAuthFromSession(User $user): void { - /** @var static $hook */ - foreach (Hook::all(self::NAME) as $hook) { + foreach (static::all() as $hook) { try { $hook->onAuthFromSession($user); } catch (Throwable $e) { @@ -82,8 +80,7 @@ public static function triggerAuthFromSession(User $user): void */ public static function triggerLogin(User $user) { - /** @var AuthenticationHook $hook */ - foreach (Hook::all(self::NAME) as $hook) { + foreach (static::all() as $hook) { try { $hook->onLogin($user); } catch (\Exception $e) { @@ -100,8 +97,7 @@ public static function triggerLogin(User $user) */ public static function triggerLogout(User $user) { - /** @var AuthenticationHook $hook */ - foreach (Hook::all(self::NAME) as $hook) { + foreach (static::all() as $hook) { try { $hook->onLogout($user); } catch (\Exception $e) { diff --git a/library/Icinga/Application/Hook/ConfigFormEventsHook.php b/library/Icinga/Application/Hook/ConfigFormEventsHook.php index 0e9f9bcbea..a315f82d36 100644 --- a/library/Icinga/Application/Hook/ConfigFormEventsHook.php +++ b/library/Icinga/Application/Hook/ConfigFormEventsHook.php @@ -103,14 +103,13 @@ private static function runEventMethod($eventMethod, Form $form) { self::$lastErrors = []; - if (! Hook::has('ConfigFormEvents')) { + if (! static::isRegistered()) { return true; } $success = true; - foreach (Hook::all('ConfigFormEvents') as $hook) { - /** @var self $hook */ + foreach (static::all() as $hook) { if (! $hook->runAppliesTo($form)) { continue; } diff --git a/library/Icinga/Application/Hook/HealthHook.php b/library/Icinga/Application/Hook/HealthHook.php index 1b775fcc40..4ba047858f 100644 --- a/library/Icinga/Application/Hook/HealthHook.php +++ b/library/Icinga/Application/Hook/HealthHook.php @@ -6,7 +6,6 @@ namespace Icinga\Application\Hook; use Exception; -use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Exception\IcingaException; @@ -150,9 +149,7 @@ public function setUrl(Url $url) final public static function collectHealthData() { $checks = []; - foreach (Hook::all('health') as $hook) { - /** @var self $hook */ - + foreach (static::all() as $hook) { try { $hook->checkHealth(); $url = $hook->getUrl(); diff --git a/library/Icinga/Application/MigrationManager.php b/library/Icinga/Application/MigrationManager.php index 1f03eace1b..2edf267d7d 100644 --- a/library/Icinga/Application/MigrationManager.php +++ b/library/Icinga/Application/MigrationManager.php @@ -302,8 +302,7 @@ protected function load(): void { $this->pendingMigrations = []; - /** @var DbMigrationHook $hook */ - foreach (Hook::all('DbMigration') as $hook) { + foreach (DbMigrationHook::all() as $hook) { if (empty($hook->getMigrations())) { continue; } diff --git a/library/Icinga/File/Pdf.php b/library/Icinga/File/Pdf.php index d3c764b2a4..7f28fcce74 100644 --- a/library/Icinga/File/Pdf.php +++ b/library/Icinga/File/Pdf.php @@ -8,6 +8,7 @@ use Dompdf\Dompdf; use Dompdf\Options; use Exception; +use Icinga\Application\Hook\PdfexportHook; use Icinga\Application\Icinga; use Icinga\Exception\ProgrammingError; use Icinga\Util\Environment; @@ -56,7 +57,7 @@ public function renderControllerAction($controller) $request = $controller->getRequest(); - if (Hook::has('Pdfexport')) { + if (PdfexportHook::isRegistered()) { $pdfexport = Hook::first('Pdfexport'); $pdfexport->streamPdfFromHtml($html, sprintf( '%s-%s-%d', diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php index dc18f98385..531129a6fb 100644 --- a/library/Icinga/Web/StyleSheet.php +++ b/library/Icinga/Web/StyleSheet.php @@ -6,6 +6,7 @@ namespace Icinga\Web; use Exception; +use Icinga\Application\Hook\ThemeLoaderHook; use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Authentication\Auth; @@ -313,7 +314,7 @@ public static function getThemeFile($theme) $app = Icinga::app(); if ($theme && $theme !== self::DEFAULT_THEME) { - if (Hook::has('ThemeLoader')) { + if (ThemeLoaderHook::isRegistered()) { try { $path = Hook::first('ThemeLoader')->getThemeFile($theme); } catch (Exception $e) { diff --git a/library/Icinga/Web/Widget/Tabextension/OutputFormat.php b/library/Icinga/Web/Widget/Tabextension/OutputFormat.php index 7fd2802e97..729d77cd1e 100644 --- a/library/Icinga/Web/Widget/Tabextension/OutputFormat.php +++ b/library/Icinga/Web/Widget/Tabextension/OutputFormat.php @@ -5,6 +5,7 @@ namespace Icinga\Web\Widget\Tabextension; +use Icinga\Application\Hook\PdfexportHook; use Icinga\Application\Platform; use Icinga\Application\Hook; use Icinga\Web\Url; @@ -84,7 +85,7 @@ public function getSupportedTypes() { $supportedTypes = array(); - $pdfexport = Hook::has('Pdfexport'); + $pdfexport = PdfexportHook::isRegistered(); if ($pdfexport || Platform::extensionLoaded('gd')) { $supportedTypes[self::TYPE_PDF] = array( From 844af8c17a28b0350f5f8674a29fe739fda3499a Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Tue, 17 Feb 2026 11:27:11 +0100 Subject: [PATCH 4/4] Deprecate `Module#provideHook()` and `Hook` methods in favor of `Hook\Essentials` ones --- library/Icinga/Application/Hook.php | 8 ++++++++ library/Icinga/Application/Modules/Module.php | 2 ++ 2 files changed, 10 insertions(+) diff --git a/library/Icinga/Application/Hook.php b/library/Icinga/Application/Hook.php index 95998bc071..387704f435 100644 --- a/library/Icinga/Application/Hook.php +++ b/library/Icinga/Application/Hook.php @@ -68,6 +68,8 @@ public static function clean() * @param string $name One of the predefined hook names * * @return bool + * + * @deprecated Use {@link Hook\Essentials::isRegistered} instead */ public static function has($name) { @@ -267,6 +269,8 @@ private static function assertValidHook($instance, $name) * @param string $name One of the predefined hook names * * @return array + * + * @deprecated Use {@link Hook\Essentials::all} instead */ public static function all($name): array { @@ -291,6 +295,8 @@ public static function all($name): array * @param string $name One of the predefined hook names * * @return null|mixed + * + * @deprecated Use {@link Hook\Essentials::first} instead */ public static function first($name) { @@ -314,6 +320,8 @@ public static function first($name) * @param string $class Your class name, must inherit one of the * classes in the Icinga/Application/Hook folder * @param bool $alwaysRun To run the hook always (e.g. without permission check) + * + * @deprecated Use {@link Hook\Essentials::register} instead */ public static function register($name, $key, $class, $alwaysRun = false) { diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 5fb7428af9..26bdf95033 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -1432,6 +1432,8 @@ protected function slashesToNamespace($class) * @param bool $alwaysRun To run the hook always (e.g. without permission check) * * @return $this + * + * @deprecated Use {@link Hook\Essentials::register} instead */ protected function provideHook($name, $implementation = null, $alwaysRun = false) {