Service Detail Hooks

259 visitas Markdown

The eight hooks behind what a customer sees on a service detail: the page data, the cards, the tools, add-ons and metrics.

Overview

The service detail is the screen a customer looks at most, and much of it comes from the module: the cards, the tools, the capabilities. The filters here catch that output before it reaches the screen.

The second group is add-ons. An add-on has a status flow of its own and moves separately from the parent service: bought on its own, invoiced on its own, suspended on its own.

Reference

Changing the detail page data

filterservice.detail_data
ClientServices the whole page

Runs after the detail page's whole template data was built.

Parameters 2
$dataarrayrefAll the data going to the page. The widest place to step in: add your own key without breaking the ones there.
$servicearrayThe service in view.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:service.detail_data', 10, function (&$data, $service) {
    // Add your own data without disturbing the keys already there.
    $data['acme_health'] = Acme::health((int) ($service['id'] ?? 0));
});

Changing the dashboard cards

filterservice.dashboard.cards
ServerModule module output

Runs after the module's card definitions were built, before they reach the screen.

Parameters 2
$cardsarrayrefThe card definitions. You can add, drop or reorder them.
$moduleServerModuleThe service's module object. Use it to tell modules apart; a rule may not suit them all.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:service.dashboard.cards', 10, function (&$cards, $module) {
    // Put your card where it belongs rather than at the end.
    array_splice($cards, 1, 0, [Acme::usageCard()]);
});

Changing the tool list

filterservice.dashboard.tools
ServerModule a grouped list

Runs after the module's tool list was built.

Parameters 2
$toolsarrayrefThe grouped tool list: each group carries its own details and tools. It is not a flat list; you reach into the group.
$moduleServerModuleThe service's module object.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:service.dashboard.tools', 10, function (&$tools, $module) {
    // The list IS GROUPED: reach the tools inside a group.
    foreach ($tools as &$group)
        $group['tools'] = array_filter($group['tools'] ?? [],
            fn ($t) => ($t['key'] ?? '') !== 'rebuild');
});

Changing the domain capabilities

filterservice.detail_tabs_capabilities
ClientDomains it opens the tabs

Runs after it was decided which capabilities the domain detail tabs show.

Parameters 3
$capabilitiesarrayrefThe capability marks: DNS records, mail forwarding and the like. Every mark you write here is carried back to the screen's matching key: switching one off hides that tab.
$servicearrayThe domain service in view.
$moduleobject|nullThe provider module — it can be empty. On a domain with no module it arrives as null, so check before calling anything on it.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:service.detail_tabs_capabilities', 10,
    function (&$capabilities, $service, $module) {
        if ($module === null) return;                 // a domain with no module

        // Switching a mark off hides that tab on the screen.
        $capabilities['has_email_forwarding'] = Acme::mailAllowed($service);
    });

Learning that an add-on opened

actionservice.addon.created
Services a separate invoice

Runs after an add-on was added to a service.

Parameters 5
$addonRecordIdintThe id of the created add-on record.
$serviceIdintThe id of the parent service.
$addonIdintThe id of the add-on product. 0 means this is a domain add-on, which has no entry in the product catalogue.
$invoiceIdintThe id of the raised invoice, or 0 where there is none.
$totalAmountfloatThe total charged for the add-on.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.addon.created', 10,
    function ($addonRecordId, $serviceId, $addonId, $invoiceId, $totalAmount) {
        // An addonId of 0 marks a DOMAIN add-on, not a product.
        if ($addonId === 0) return;

        Crm::addonSold($serviceId, $addonId, (float) $totalAmount);
    });

Following an add-on status

actionservice.addon.status_changed
Services one array parameter

Runs after an add-on's status changed.

Parameters 1
$payloadarrayA single array: service, addon, addon_id, old_status, new_status, user_id. An add-on's status is independent of the parent: one can be suspended while the other runs.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.addon.status_changed', 10, function ($payload) {
    // ONE array arrives; the old and new status sit inside it.
    if (($payload['new_status'] ?? '') === 'suspended')
        Acme::addonOff((int) ($payload['addon_id'] ?? 0));
});

Stopping a metric switch

gateservice.metric_toggle
ClientServices nothing is written

Runs while a customer switches a usage metric on or off. Stopping it means nothing reaches the database or the module.

Parameters 4
$servicearrayThe service record.
$metricKeystringThe metric key. Defined in the product's metric settings.
$enableboolThe state being asked for.
$labelstringThe metric's display label. Handed to you ready for your error message.
Return 1
stringA non-empty string stops the switch; the text is thrown as the error.
Listener
Hook::add('gate:service.metric_toggle', 10,
    function ($service, $metricKey, $enable, $label) {
        // The label arrives ready: write the message with the name they see.
        if (!$enable && Acme::metricRequired($metricKey))
            return $label . ' cannot be switched off.';

        return null;
    });

Following a metric switch

actionservice.metric_toggled
ClientServices it landed

Runs after the metric state was written.

Parameters 3
$servicearrayThe service record.
$metricKeystringThe metric key.
$enableboolThe new state that was written.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.metric_toggled', 10, function ($service, $metricKey, $enable) {
    // A metric switched off can still be billed: set your own counter.
    Acme::metricState((int) ($service['id'] ?? 0), $metricKey, (bool) $enable);
});

Following a customer opening the detail page

actionservice.detail.viewed
website/services on every opening

Runs when a customer opens the management page of their own service. Ownership and access checks are already behind you.

Parameters 2
$servicearrayThe loaded service record.
$serviceIdintThe id of the service.
Return 1
voidThe return is ignored. To change what the page shows, use the detail data filter rather than this hook.
Listener
Hook::add('action:service.detail.viewed', 10, function ($service, $serviceId) {
    // Keep it light: the customer is waiting for the page.
    Acme::touchLastSeen($serviceId);
});

Following an admin opening the detail page

actionservice.viewed
admin/services on every opening

Runs when an administrator opens a service detail. It carries the same data as its customer-side twin; only the person looking differs.

Parameters 2
$servicearrayThe loaded service record.
$service_idintThe id of the service.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.viewed', 10, function ($service, $service_id) {
    // Pull the live status from the remote panel ahead of time.
    Acme::prefetchStatus($service_id);
});

Stopping a file download

gateservice.file_download
website/services three flows

Runs before a customer downloads a service file. All three flows pass through here: requirement attachment, software package and delivery file.

Parameters 2
$kindstringWhich flow: requirement, package or delivery.
$ctxarrayContext. Shared: user_id, service_id. The flow adds file name, version and record id.
Return 1
mixedA filled return blocks the download: the answer becomes a 403 at once. The trap: this endpoint returns a raw file, so your return never reaches the customer. Unlike other gates no reason appears on screen; record it yourself. An empty return lets the download go on.
Listener
Hook::add('gate:service.file_download', 10, function ($kind, $ctx) {
    if ($kind !== 'package') return null;

    // Block when the quota is used up; no reason reaches the screen, so log it.
    if (Acme::quotaExceeded((int) ($ctx['user_id'] ?? 0))) {
        Acme::log('download quota used up', $ctx);
        return true;
    }

    return null;
});

Changing which file is served

filterservice.file_download.source
website/services passed by link

Runs before the file is handed to the stream. You can build a package with install-specific keys inside it, or send the download to an address of your own.

Parameters 2
$sourcearrayby linkWhat will be served: path (full path on disk), name (the saved name), link (outside address), cleanup.
$ctxarrayby linkContext: kind, user_id, service_id, the service record and product id.
Return 1
voidThe return is ignored; you write over the source. A filled path is sent; an empty path with an outside address redirects there; neither gives a 404. Set cleanup and only the file is removed afterwards, not the folder above it.
Listener
Hook::add('filter:service.file_download.source', 10, function (&$source, &$ctx) {
    if (($ctx['kind'] ?? '') !== 'package') return;

    // Build a package with the install key inside, then have it removed.
    $source['path']    = Acme::buildPackage((int) ($ctx['service_id'] ?? 0));
    $source['name']    = 'acme-setup.zip';
    $source['cleanup'] = true;
});

Stopping a billing profile assignment

gateservice.billing_profile_assign
ClientServiceBilling before the write

Runs while a customer assigns a billing profile to a service, before anything is written. Domains pass through this gate too.

Parameters 3
$servicearrayThe service or domain record.
$profileIdintThe id of the profile being assigned. A zero means the override is being dropped and the account default restored.
$uidintThe account that owns both the profile and the service.
Return 1
string|nullA non-empty text blocks the assignment and the text is shown to the customer as the error. An empty return lets it carry on.
Listener
Hook::add('gate:service.billing_profile_assign', 10,
    function ($service, $profileId, $uid) {
        // Do not allow profile changes while an account is frozen.
        if (Acme::frozen($uid)) return 'Your account is under review, so this cannot change.';

        return null;
    });

Following a billing profile assignment

actionservice.billing_profile_assigned
ClientServiceBilling after the write

Runs once the assignment is saved. The gate is behind you and the value is written.

Parameters 4
$servicearrayThe service or domain record.
$profileIdintThe assigned profile; a zero means the default came back.
$profileNamestringThe visible name: a company or person, or "Default" when the override was dropped.
$uidintThe owning account.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.billing_profile_assigned', 10,
    function ($service, $profileId, $profileName, $uid) {
        Acme::syncAccounting((int) ($service['id'] ?? 0), $profileId);
    });

Pitfalls

The tool list arrives grouped

The tool filter hands you a list of groups, not a flat list, with each group holding its own tools. A listener walking it directly finds no tools, because every element it holds is a group.

The module can be empty on the capability filter

On the domain capability filter the third parameter can be empty: some domains have no module. Calling a method without checking takes the page down. Every mark you write here is also carried back to the screen, so switching one off makes that tab vanish.

An add-on is independent of its parent

An add-on carries its own status, its own invoice and its own suspend flow. The parent can be running while the add-on is suspended, or the other way round. Reading the parent's status and assuming the add-on is open leaves an unpaid capability switched on.

A zero add-on id means a domain

Where the product id on the add-on event is 0, this is a domain add-on (DNS management, privacy, forwarding) with no entry in the product catalogue. A listener reaching for a product finds nothing here.

¿Le ha resultado útil?

¡Gracias por sus comentarios!

¿Todavía necesita ayuda?

Nuestro equipo de soporte está disponible las 24 horas para ayudarle con lo que no encuentre aquí.