Notification Hooks

2 vues Markdown

The eight hooks behind every message reaching a customer: the dispatch gate, the recipient list, the variables and delivery.

Overview

A notification passes two gates. The first stops the dispatch itself: it reaches nobody. The second stops one delivery: the other recipients still get the message.

Between them sit the recipient list and the variables. The variables filter runs per recipient, so one notification can reach each person with different content.

Reference

Stopping the whole dispatch

gatenotification.dispatch
Notification::dispatch() a different return contract

Runs before the notification is built. Stopping it means the message reaches nobody.

Parameters 3
$groupstringThe notification group: invoice, service, order, domain, tickets.
$namestringThe notification key: invoice-created, welcome and the like.
$contextarrayThe raw context: the record ids involved, extra parameters, manually added recipients, forced channels.
Return 1
mixedAny non-empty return stops the dispatch — even true. Unlike the other gates there is no text requirement here; returning a value by accident cuts the notification quietly.
Listener
Hook::add('gate:notification.dispatch', 10, function ($group, $name, $context) {
    // MIND: ANY non-empty return stops the dispatch, even true.
    if ($name === 'invoice-reminder' && Acme::quietHours()) return true;

    return null;
});

Changing the recipient list

filternotification.recipients
Notification a row per channel

Runs after the recipients were resolved, before the messages are built.

Parameters 3
$recipientsarrayrefThe recipient rows: channel, account, address, name, cc, language. One person can appear on several rows: e-mail and SMS are separate.
$groupstringThe notification group: invoice, service, order, domain, tickets.
$namestringThe notification key: invoice-created, welcome and the like.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:notification.recipients', 10,
    function (&$recipients, $group, $name) {
        // One person has SEPARATE rows for e-mail and SMS: filter by channel.
        if ($name === 'invoice-created')
            $recipients = array_filter($recipients,
                fn ($r) => ($r['channel'] ?? '') !== 'sms');
    });

Changing the template variables

filternotification.render_variables
Notification runs per recipient

Runs before the message text is produced, separately for each recipient.

Parameters 4
$variablesarrayrefThe variable set for this recipient. The placeholders in the template fill from here.
$groupstringThe notification group: invoice, service, order, domain, tickets.
$namestringThe notification key: invoice-created, welcome and the like.
$recipientarrayThe recipient in hand: channel, language, account, address. The language field matters: produce your text in theirs.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:notification.render_variables', 10,
    function (&$variables, $group, $name, $recipient) {
        // It runs PER RECIPIENT: produce your text in their language.
        $variables['acme_note'] = Acme::note($recipient['lang'] ?? 'en');
    });

Stopping a single delivery

gatenotification.deliver
Notification the message is ready

Runs immediately before a built message goes out. Stopping it cuts this delivery alone.

Parameters 1
$itemarrayThe item about to be delivered: channel, account, address, name, cc, subject, body, attachments, reason. The text and attachments are ready: this is where a last check belongs.
Return 1
stringA non-empty string stops this delivery; the other recipients still get theirs.
Listener
Hook::add('gate:notification.deliver', 10, function ($item) {
    // It stops THIS delivery alone: the others carry on.
    if (Acme::bounced($item['recipient'] ?? '')) return 'the address is dead';

    return null;
});

Following a dispatch

actionnotification.dispatched
Notification one array parameter

Runs after the notification went out.

Parameters 1
$payloadarrayA single array: group, name, account, variables, batch id. The batch id ties every message of one dispatch together.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:notification.dispatched', 10, function ($payload) {
    // The batch id ties every message of one dispatch together.
    Acme::trackBatch($payload['batch_id'] ?? '', $payload['name'] ?? '');
});

Widening the template fields

filternotification.template_merge_fields
AdminNotifications the list shown to the operator

Runs while the available fields are listed on the template editing screen.

Parameters 1
$fieldsarrayrefThe field list shown to the operator. Adding one here shows it in the list; producing its value wants the variables filter too.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:notification.template_merge_fields', 10, function (&$fields) {
    // Adding to the list produces NO value: write the variables filter as well.
    $fields['acme_note'] = 'Acme note';
});

Changing the invoice attachment name

filternotification.invoice_pdf_name
Notification the file name

Runs while the invoice document is attached to a notification.

Parameters 1
$namestringrefThe attachment's file name. The name on the customer's machine, so keep to safe characters.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:notification.invoice_pdf_name', 10, function (&$name) {
    $name = Acme::safeFileName($name);
});

Changing a template label

filternotification.template_label
AdminNotifications it shows in the panel

Runs while a notification template's name in the panel is resolved.

Parameters 1
$labelstringrefThe template's display name. Only the operator sees it; it does not touch the text reaching a customer.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:notification.template_label', 10, function (&$label) {
    $label = Acme::prefixLabel($label);
});

Changing the invoice lines

filternotification.invoice_items
Notification::build_items changes by returning

Runs before the invoice lines reach the template. You can add lines, drop them, reorder them or put a field of your own on each one.

Parameters 3
$resultarrayThe lines prepared for the template. This is what you change.
$invoicearrayThe invoice itself.
$itemsarrayThe raw lines as they came from the database. Use this to recover a field the prepared list dropped.
Return 1
array|nullAn array you return replaces the whole list. The trap: with several listeners the last return wins and earlier additions vanish. To add, take the incoming list and build on it.
Listener
Hook::add('filter:notification.invoice_items', 10, function ($result, $invoice, $items) {
    // Keep the incoming list: building from scratch erases what others added.
    foreach ($result as $i => $line)
        $result[$i]['acme_note'] = Acme::noteFor($line['id'] ?? 0);

    return $result;
});

Changing the order lines

filternotification.order_items
Notification::build_order_items changes by returning

The same job for order notifications. Lines arrive already split by quantity, with live add-ons loaded.

Parameters 2
$itemsarrayThe lines prepared for the template.
$orderarrayThe order itself, raw line data included.
Return 1
array|nullAn array replaces the whole list; anything else is ignored.
Listener
Hook::add('filter:notification.order_items', 10, function ($items, $order) {
    // Hide internal lines from the customer.
    return array_values(array_filter($items, fn ($x) => ($x['name'] ?? '') !== 'internal'));
});

Masking the delivery record

filternotification.log_entry
LogManager passed by link personal data

Runs right before an email or SMS record is written to the database. This is where you mask personal data, or keep no record at all.

Parameters 1
$entryarrayby linkThe record about to be written. Shared fields: channel (mail or sms), user_id, reason, content, data, private. Email also carries subject.
Return 1
voidThe return is ignored; you change the array in place. Set $entry['abort'] = true and nothing is written; the counter comes back zero. The message still goes out, only the trace is dropped.
Listener
Hook::add('filter:notification.log_entry', 10, function (&$entry) {
    // Never store the body of a password reset.
    if (($entry['reason'] ?? '') === 'password-reset') {
        $entry['content'] = '[masked]';
        return;
    }

    // Open no record at all for campaign texts.
    if (($entry['channel'] ?? '') === 'sms' && ($entry['reason'] ?? '') === 'campaign')
        $entry['abort'] = true;
});

Giving the preview sample values

filternotification.preview_variables
AdminNotifications passed by link

Runs while a template is previewed in the panel. The preview sends nothing and knows only the templates that ship with the core, so fields of a template you added come out empty. Supply sample values here.

Parameters 2
$variablesarrayby linkWhat the template will receive, name against value. Add your own fields and leave the existing ones alone.
$ctxarrayby linkWhich template is on screen: group, template, lang. Leave without touching anything when it is not yours.
Return 1
voidThe return is ignored; you add values by writing into the array.
Listener
Hook::add('filter:notification.preview_variables', 10, function (&$variables, &$ctx) {
    // Care only about your own template.
    if (($ctx['template'] ?? '') !== 'acme-welcome') return;

    $variables = array_merge($variables, [
        'acme_plan'  => 'Starter',
        'acme_quota' => '10 GB',
    ]);
});

Pitfalls

The dispatch gate reads any return as a block

The other gates want a non-empty string; this one reads any non-empty value as a block. Returning something by accident at the end of your closure — even true — means the notification never goes, with no trace anywhere.

One person appears more than once

The recipient list carries a row per channel: a customer on both e-mail and SMS sits on two rows. A listener deduplicating by account id quietly switches off a whole channel.

The variables filter runs once per recipient

On a notification reaching ten people this filter runs ten times. A query or a remote call inside it turns one notification into ten requests. Prepare the shared data once and produce only the per-recipient part inside.

Listing a field produces no value

The template fields filter widens the list shown to the operator and nothing else. Add a field without writing the variables filter and the operator puts it in a template, where it reaches the customer empty. The two are written together.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.