Common Hook Scenarios
Six common jobs under six different contracts: catching an event, blocking, adding to a screen, changing a value, carrying a variable, registering a route.
Overview
The six scenarios below cover all five families and all six behave differently. Two have you change a value by reference and one has you return it. Two have you touch an object handed to you. One has you return text and cut the action short.
The spread is deliberate. The shortest way to show that a hook's expectations do not follow from its family is two different contracts inside one family.
Prerequisites
- Writing a Hook Listener read first.
- The hook names in the examples are real in this installation; use the domain directory when adapting them to your own job.
The Scenarios
1. Tell the outside when an order is placed
This runs the moment an order is placed. The invoice is still unpaid here: a zero-total order, or one settled from the balance, closes later in the same request. Use the payment hook where you want an order that is paid for.
Hook::add('action:order.checkout_completed', 10,
function ($order_id, $invoice_id, $pmethod, $member) {
Crm::orderPlaced($order_id, [
'invoice' => $invoice_id, // not paid yet
'method' => $pmethod, // 'Free' on a zero-total order
'email' => $member['email'] ?? '',
]);
});
2. Block a sign-in
This runs after the credentials check: the password is already right and you stop the sign-in for some other reason. The hook takes plain text as well as the ['message' => …] form, and an empty return lets the sign-in carry on.
Hook::add('gate:user.login', 10, function ($user_id, $check, $ip) {
if (!OfficeNetwork::covers($ip))
return 'The panel is reachable from the office network only.';
return null;
});
3. Add a tab to the customer detail
This is a screen hook, and yet you return no HTML: you are handed a tab object and you add to it. The return is not read. Most hooks in this family want HTML. This one wanting an object is the clearest case of a contract you cannot read off the family.
Hook::add('ui:admin.client_detail.tabs', 10, function ($tab, $user, $user_id) {
$tab->add('acme', 'Acme Records', AcmePanel::render($user_id));
});
4. Change an order total
The totals array arrives by reference, and the total key is the amount that goes on the order. Touch the tax keys and the invoice lines follow them.
Hook::add('filter:order.total', 10, function (&$tax_calc, $subtotal, $total_discount) {
if ($subtotal >= 1000) $tax_calc['total'] = round($tax_calc['total'] * 0.95, 2);
});
5. Carry a variable into every template
This one does not work by reference: you return the changed array. What you return is not merged with the existing variables, it replaces them. So build on the array you were given and return all of it.
Hook::add('filter:template.variables', 10, function ($template_path, $data) {
$data['acme_banner'] = AcmeBanner::current(); // built on what we were given
return $data; // ALL of it returned
});
6. Register a new route
You are handed the router object and you register on it; the return is not read. This is how modules open pages of their own.
Hook::add('register:routes', 10, function ($router) {
$router->add('acme-report', 'acme/report/(?)', 'Acme:report');
});
Example
All six can sit in one file. An add-on's whole set of bindings usually lives in a single hooks.php, with every line under a different contract.
<?php
// an event: the return is ignored
Hook::add('action:invoice.payment_recorded', 10,
function ($payment_id, $invoice_id, $payment) {
Crm::paymentSeen($invoice_id, (float) ($payment['amount_in'] ?? 0));
});
// a value: by reference
Hook::add('filter:order.total', 20, function (&$tax_calc, $subtotal) {
if (AcmeCampaign::active()) $tax_calc['total'] = round($tax_calc['total'] * 0.9, 2);
});
// a value: by return, the WHOLE array
Hook::add('filter:template.variables', 20, function ($template_path, $data) {
$data['acme_campaign'] = AcmeCampaign::active();
return $data;
});
// a route: registered on the object
Hook::add('register:routes', 20, function ($router) {
$router->add('acme-report', 'acme/report/(?)', 'Acme:report');
});
Pitfalls
Returning only the key you added on the variables hook wipes every other variable going to the template. The return is not merged, it replaces. With more than one listener the last return wins, so a listener after yours can wipe yours too. Build on the array you were given and return all of it.
The hook running when an order is placed sees the invoice unpaid. Orders settled from the balance, and zero-total ones, close later in that same request. Bind to the payment hook where the work depends on money arriving. Asking "is it paid" inside the order hook always answers no.
Most hooks in the screen family expect HTML to print. Some hand you an object and want you to touch that instead, ignoring your return. Tab, menu and page-part hooks are usually of the second kind. Returning HTML and seeing nothing is most often this.
The sign-in gate runs after the credentials check. The user record there is real and verified; the gate's job is not checking a password but stopping the sign-in for another reason: a network limit, working hours, a maintenance window.
Related Articles
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.