How Hooks Work
The way into the system's flow without touching a core file: a name, a running order and a function to run.
Overview
A hook is the point where the core stops and asks "has anyone here got something to say?" An order is saved and it asks. An invoice total is worked out and it asks. A management screen is about to be drawn and it asks. You leave a function behind, and the core calls it on reaching that point.
What this buys you: behaviour added without editing the core. An edited core file comes back on the first update. A hook listener sits in your own file, and the update leaves it alone. This is also how modules bind themselves to the system.
This installation holds 984 hook points spread over seventeen domains by subject. The word in front of the name says what it was opened for.
Prerequisites
- A file for your listener to live in: a
.phpfile in the installation's owncoremio/hooksdirectory, or a module'shooks.php. - A finished installation. No listener loads before the setup wizard is done (see the pitfalls below).
- The exact name of the hook you are binding to. A wrong name does nothing at all and reports nothing either.
Structure
Listener files are read on their own and you register them nowhere. Two places are swept and both load together.
coremio/hooks/*.php # the installation's own listeners
coremio/modules/{Type}/{Name}/hooks.php # every module's own listeners
Loading happens when the first hook runs, not at page start. The files are read once and stay in memory for that request. So adding a listener restarts nothing: you drop the file in and it runs on the next request.
A hook takes as many listeners as you like. They all run by running order, and the smaller number goes first.
Reference
Registering a listener
// The Hook class — coremio/classes/Hook.php
static function add(string $name, int $priority, callable|array $properties): void;
The four listener forms
// 1 — a closure: the common one
Hook::add('action:order.created', 10, function ($order) {
Crm::push($order['id']);
});
// 2 — a class method: the class is built ONCE and shared across every hook
Hook::add('filter:invoice.late_fee_amount', 10, ['class' => 'AcmeBilling', 'method' => 'adjust']);
// 3 — a static method: no object is built
Hook::add('gate:order.checkout', 10, ['class' => 'AcmeGuard', 'method::static' => 'allow']);
// 4 — the constructor: with NO method key the class is built and THE OBJECT is the return
Hook::add('register:routes', 10, ['class' => 'AcmeRoutes']);
How the core runs a hook
static function run(string $name, mixed ...$args): array;
static function runRefs(string $name, mixed &...$args): array;
static function runDetailed(string $name, mixed ...$args): array;
&$x in its signature changes the caller's variable. This is how the filter family works.
What all three share: a listener that throws is caught, recorded, and the next one runs. A single broken listener stops neither the core nor the other listeners.
Example
One file, three listeners, three different jobs: catching an event, changing a value and stopping an action.
<?php
// 1 — AN EVENT: an order was written, tell the outside. Our return is not read.
Hook::add('action:order.created', 10, function ($order) {
Crm::push((int) $order['id'], $order);
});
// 2 — A VALUE: change the row. With & on the first argument the caller's variable changes.
Hook::add('filter:admin.table.rows', 10, function (&$row, $table) {
if ($table === 'services') $row['name'] = strtoupper($row['name'] ?? '');
});
// 3 — PERMISSION: returning a non-empty string stops the action and that text reaches the user.
Hook::add('gate:order.checkout', 10, function ($member, $items) {
if (Blocklist::has((int) ($member['id'] ?? 0)))
return 'This account cannot order.';
return null; // null or an empty string: the flow carries on
});
It ends the moment you drop the file in. There is no registering, no build and no restart; all three run on the next request.
Pitfalls
Binding a listener to a hook name that does not exist reports nothing; the listener never runs. Where a listener "does not work", the first place to look is not the code but the name. Check it against the hook index rather than writing it from memory.
The core binds a listener's parameters in order and skips an empty (null) argument. The gap does not close: everything after it slides one to the left and the second parameter lands where the first was. Never pass an optional context value as empty; pass nothing, or a placeholder such as an empty string or an empty array.
The gathered array carries the non-empty returns alone. Where one of three listeners returns empty, the array holds two items and the first item is no longer the first listener. Taking "the first result" is safe on hooks with a single listener and nowhere else.
Writing a three-parameter listener where the hook throws two values makes the call fail. The failure is caught and recorded, and the listener is skipped. What you see from outside is "it does not work". Check how many values the listener takes against the hook's entry. Writing fewer parameters is safe; writing more is not.
A name starting with filter: is no guarantee the value arrives by reference. On measuring, part of the filter family was being called with a copy. Do not read off its name whether a hook can truly change its value. Read it from the mechanism line in the hook's entry.
While the setup stamp is empty, none of the hook files are read. Do not expect to reach into the setup wizard's flow with a hook; that ground comes before hooks exist.
Related Articles
Vielen Dank für Ihre Rückmeldung!
Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.