Service Status Hooks

2 vues Markdown

Nine hooks from a sold service opening to its record going: setting up, status changes, suspending, terminating and deleting.

Overview

A service lives in two places at once: as a row in our database and as a real account at the end of a provider module. Most hooks here speak about the first.

Knowing that split matters: when a status changes, our record changed. Whether the account on the server truly closed is a separate question, and usually the outcome of a job that was queued.

Reference

Changing the service about to be created

filterservice.save_data
Hook::runRefs by reference

Runs before the service row is written to the database.

Parameters 1
$dataarrayrefThe row about to be written: user_id, product_id, options, status, duedate. What you change here goes straight to the database.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:service.save_data', 10, function (&$data) {
    // Put your own tracking key inside options; do not add a column.
    $data['options']['acme_batch'] = Acme::currentBatch();
});

Learning that a service opened

actionservice.created
Services::create() the row landed

Runs after the service row was written. The account at the provider may not exist yet at this point.

Parameters 2
$idintThe new service id.
$dataarrayThe data that was written. It has passed the filter, so your changes show here.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.created', 10, function ($id, $data) {
    // The row exists, the account may not: do not wait on provisioning here.
    Crm::opened((int) $id, (int) ($data['user_id'] ?? 0));
});

Stopping a status change

gateservice.status_change
Services::change_status() both states

Runs before the service status changes, handing you both the old state and the target.

Parameters 3
$servicearrayThe service record with its current status.
$statusstringThe target: active, suspended, cancelled, inprocess.
$oldStatusstringThe current status. Read both together to target one particular transition.
Return 1
stringA non-empty string stops the transition; the text is thrown as the error.
Listener
Hook::add('gate:service.status_change', 10,
    function ($service, $status, $oldStatus) {
        // Check only the reopening of a cancelled service.
        if ($oldStatus === 'cancelled' && $status === 'active'
            && !Acme::reactivationAllowed($service))
            return 'A cancelled service reopens with an operator\'s approval.';

        return null;
    });

Following a status change

actionservice.status_changed
Services::change_status() the module may have overridden

Runs after the status changed. The value you get is the final one, after any override by the module.

Parameters 4
$serviceIdintThe service id.
$statusstringThe new status. It can differ from the one asked for where the module stepped in.
$oldStatusstringThe previous status.
$servicearrayA snapshot of the service before the change.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.status_changed', 10,
    function ($serviceId, $status, $oldStatus, $service) {
        // Do no work again where it landed back on the same status.
        if ($status === $oldStatus) return;

        Crm::statusMoved($serviceId, $oldStatus, $status);
    });

Stopping a suspend

gateservice.suspend
cronjobs/ServiceSuspend a veto turns into a cancel

Runs before a service or add-on is suspended. Stopping it has a cost: the job turns into a cancel signal.

Parameters 4
$target_typestringservice or addon. One hook carries both.
$target_idintThe id of the record being suspended.
$servicearrayThe live row: status, duedate, module, owner_id.
$user_idintThe service owner.
Return 1
stringA non-empty string vetoes the suspend — and it does not end there: the signal turns into a cancel and your reason is recorded. So "do not suspend" is not the same as "do nothing".
Listener
Hook::add('gate:service.suspend', 10,
    function ($target_type, $target_id, $service, $user_id) {
        if ($target_type !== 'service') return null;      // leave add-ons alone

        // MIND: a veto stops the suspend but turns the job into a CANCEL signal.
        if (Acme::vipAccount($user_id)) return 'VIP account: review by hand.';

        return null;
    });

Following a suspend

actionservice.suspended
cronjobs/ServiceSuspend the module job is queued

Runs after the service was suspended. The actual closing on the server was queued.

Parameters 5
$target_typestringservice or addon.
$target_idintThe id of the suspended record.
$user_idintThe service owner.
$reasonstringThe reason, translated into the customer's language.
$module_queue_idintThe queue id of the module job. 0 means it was never queued: on a service without a module nothing happens on the server side.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.suspended', 10,
    function ($target_type, $target_id, $user_id, $reason, $module_queue_id) {
        // A queue id of 0 means nothing was done on the server.
        if ($module_queue_id === 0) Ops::note('suspend-no-module', $target_id);
    });

Following a termination

actionservice.terminated
cronjobs/ServiceTerminate the server side

Runs after the service was terminated. This hook carries the server side: which module, which server.

Parameters 5
$target_idintThe id of the terminated service.
$service_typestringhosting or server.
$modulestringThe server module's name.
$server_idintThe server the service sat on.
$module_queue_idintThe queue id of the termination job.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.terminated', 10,
    function ($target_id, $service_type, $module, $server_id, $module_queue_id) {
        // Capacity came free on that server: update your own counter.
        Capacity::released((int) $server_id, $service_type);
    });

Stopping a service delete

gateservice.delete
Services::delete() the record goes

Runs before the service record is deleted. Deleting is not the same as closing the account on the server: it removes our record alone.

Parameters 1
$servicearrayThe service record about to be deleted.
Return 1
stringA non-empty string stops the delete; the text is thrown as the error.
Listener
Hook::add('gate:service.delete', 10, function ($service) {
    // Deleting a live service leaves an orphaned account on the server.
    if (($service['status'] ?? '') === 'active')
        return 'A live service cannot be deleted; terminate it first.';

    return null;
});

Following a delete

actionservice.deleted
Services::delete() a last snapshot

Runs after the service record was deleted.

Parameters 2
$idintThe id of the deleted service.
$servicearrayA snapshot from before the delete. Take what you need from here; the record is gone.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.deleted', 10, function ($id, $service) {
    Acme::forgetService((int) $id, $service['name'] ?? '');
});

Pitfalls

A suspend veto becomes a cancel

Returning a non-empty text at the suspend gate does not stop the work, it turns it into a cancel. A rule written as "do not suspend this one" can end with the service cancelled outright. Write here knowing what follows.

A queue id of zero means nothing happened on the server

The suspend and terminate hooks hand you a queue id. Zero means the module job was never created: the service has no module, or the server is unknown. Our record changed and nothing happened on the server.

The final status may not be the one asked for

The status event hands you the value after any override by the module. The target you saw at the gate and the value in the event can differ. Reacting to a transition, read the value from the event rather than the intent at the gate.

Deleting is not closing

Deleting a service removes our record and nothing else. The account on the server stays where it is, now visible from nowhere. Where the server side should close too, terminate first and delete after.

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.