Service Module Hooks

1 views Markdown

The eight hooks talking to the module that truly provides a service: the gates before a call, the parameters going out and the result coming back.

Overview

The code opening an account on a server, suspending it or changing a password lives in the module. The core says "do this" and waits for the outcome.

This path has two separate gates and mixing them up is common: one for calls from the panel, one for calls from the customer's own panel. The customer path is narrower and cannot reach past the methods a module openly allows.

Reference

Changing the options a service is built with

filterservice.build_options
Orders::buildServices() from order to service

Runs while a service is built from an order item, before the record is written.

Parameters 3
$service_dataarrayrefThe whole payload of the service to be built: type, product_id, amount, status, module, options, metrics.
$itemarrayThe source order item. Read-only context; what the customer picked at order time is here.
$productarrayThe resolved product row. Read-only context.
Return 1
voidThe values change by reference; the return is not read.
Listener
Hook::add('filter:service.build_options', 10,
    function (&$service_data, $item, $product) {
        // Fill in module options from what the order says.
        $service_data['options']['acme_region'] = Acme::region($item);
    });

Changing what the module is built with

filterservice.module_context
Hook::runRefs before the module object exists

Runs in Services::run_module(), immediately before the module object is built. The module reads $service['options'] at construction time, so this is the last point where a change still reaches the provider — the gate below already runs too late for that.

Parameters 3
$servicearrayrefThe service record. Its options is what the module object is built from.
$actionstringrefThe action about to run. Alias resolution (terminatecancel) happens after this hook, so a new value is resolved too.
$paramsarrayrefThe arguments the module method will receive.
Return 1
voidThe values change by reference; the return is not read.
Listener
Hook::add('filter:service.module_context', 10,
    function (&$service, &$action, &$params) {
        if ($action !== 'create') return;

        // Written into the copy the module is about to be built from; persist it
        // separately if it also has to survive in the record.
        $options = is_array($service['options'] ?? null) ? $service['options'] : [];
        $options['ip'] = Acme::reserve((int) $service['id']);
        $service['options'] = $options;
    });
Every create path passes here, and only here

The up/downgrade rebuild hands run_module() a prepared array and never re-reads the row. A listener that only follows action:service.created misses that path, and the service is provisioned without whatever the listener meant to add.

Stopping a module action

gateservice.module_action
Services::run_module() the panel path

Runs before the module method is called. This gate stands on calls from the panel.

Parameters 3
$servicearrayThe service record.
$actionstringThe action about to run: create, suspend, cancel and the like.
$paramsarrayThe parameters going to the module method.
Return 1
stringA non-empty string stops the action; the text is thrown as the error and the module is never called.
Listener
Hook::add('gate:service.module_action', 10, function ($service, $action, $params) {
    // Send no write action to a server inside its maintenance window.
    if (in_array($action, ['create', 'suspend', 'cancel'], true)
        && Acme::maintenance((int) ($service['server_id'] ?? 0)))
        return 'The server is under maintenance; this cannot run now.';

    return null;
});

Following a module call

actionservice.module_ran
Services::run_module() one array parameter

Runs after the module method ran — whether it worked or not.

Parameters 1
$payloadarrayIt carries a single array: service, instance (the module object), action, result, error. Unlike the other hooks the parameters do not arrive separately; they all sit inside this one.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.module_ran', 10, function ($payload) {
    // ONE parameter arrives; the error field inside it reports the failure.
    if (!empty($payload['error']))
        Ops::alert('module-failed', $payload['action'] ?? '', $payload['error']);
});

Changing the module result

filterservice.module_result
Hook::runRefs all five by reference

Runs before the module's return is handled by the core. All five arguments arrive by reference.

Parameters 5
$servicearrayrefThe service record.
$instanceobjectrefThe module object.
$actionstringrefThe action that ran.
$resultmixedrefWhat the module returned: a config array, login details, false, or something else. This is the one you usually change.
$errormixedrefThe error message or null. Writing here tells the core "this failed".
Return 1
voidThe values change by reference; the return is not read.
Listener
Hook::add('filter:service.module_result', 10,
    // Defaults are REQUIRED: $error is null when the module succeeds, and a null argument is
    // dropped, so a listener with five required parameters never runs on a successful call.
    function (&$service = null, &$instance = null, &$action = null, &$result = null, &$error = null) {
        // Turn a provider's temporary error into something worth retrying.
        if ($error && Acme::transient((string) $error)) {
            $error  = null;
            $result = false;          // the core reads a failure, raises no alarm
        }
    });

Stopping a call from the customer panel

gateservice.client_tool
ClientServices the customer path

Runs where a customer calls a module action from their own panel. This gate is separate from the panel one.

Parameters 3
$servicearrayThe customer's own service.
$methodNamestringThe real module method about to run: a directly callable name (sso_panel_login) or the handle_ form.
$moduleobjectThe module object. Built in customer mode, with panel-only rows already dropped.
Return 1
stringA non-empty string stops the call; it reaches the customer as the error.
Listener
Hook::add('gate:service.client_tool', 10, function ($service, $methodName, $module) {
    // One-click panel entry is sensitive: close it to unverified accounts.
    if ($methodName === 'sso_panel_login' && !Acme::verified($service))
        return 'Verify your account before entering the panel.';

    return null;
});

Following a customer call

actionservice.client_tool_ran
ClientServices two method names

Runs after the module method the customer called has run.

Parameters 4
$servicearrayThe service record.
$methodstringThe name at request level: tool_action, tool_table, sso_panel_login, or a key the module opened.
$methodNamestringThe module method that actually ran. The two often differ, so mind which one you read.
$moduleResultmixedWhat the module returned. Output the module printed directly is not in here.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.client_tool_ran', 10,
    function ($service, $method, $methodName, $moduleResult) {
        // The request name and the real method differ: record BOTH.
        Audit::clientTool((int) ($service['id'] ?? 0), $method, $methodName);
    });

Changing the configuration fields

filterservice.config_fields
Hook::runRefs fields and actions

Runs after the configuration screen the module offers was built.

Parameters 2
$configarrayrefIt carries two keys: actions and fields. You can add fields, drop them or make them read-only.
$moduleServerModuleThe service's module object.
Return 1
voidThe values change by reference; the return is not read.
Listener
Hook::add('filter:service.config_fields', 10, function (&$config, $module) {
    // Show a dangerous action to nobody but an operator.
    unset($config['actions']['rebuild']);
});

Following a service password change

actionservice.password_changed
ClientServices the password is not carried

Runs after the service's panel password changed.

Parameters 2
$servicearrayThe service whose password changed.
$uidintThe account id of the service owner.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.password_changed', 10, function ($service, $uid) {
    // The new password NEVER reaches the hook; report only that it changed.
    Notify::securityEvent($uid, 'service-password', (int) ($service['id'] ?? 0));
});

Following a server change

actionservice.server_changed
AdminServices after the move

Runs after a service is assigned to a different server. The record moved, but nobody moved the data: this hook is where you start that.

Parameters 4
$idintThe id of the service.
$currentServerIdintThe previous server; a zero means it had none.
$new_server_idintThe new server; a zero means it was taken off one.
$newServerTypestringThe module type now set on the service.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.server_changed', 10,
    function ($id, $currentServerId, $new_server_id, $newServerType) {
        // The record moved, the data did not: start the migration yourself.
        if ($currentServerId && $new_server_id)
            Acme::queueMigration($id, $currentServerId, $new_server_id);
    });

Pitfalls

There are two gates and neither covers the other

Calls from the panel pass one gate and calls from the customer panel pass another. A rule bound to only one can be walked around by the other path. Where your rule holds for both sides, bind to both.

The module event carries one array

Where other hooks hand you separate parameters, the module-ran hook hands you one array. A listener expecting four parameters fails not because the hook throws two, but because it throws one. Reading the error field inside it is also the only way to tell success from failure.

The result filter can write the error too

On the result filter all five arguments are by reference: you can change the result and the error. Clearing the error tells the core "this did not fail"; used carelessly it hides a real failure and nobody notices.

The customer path is narrow already

From the customer panel only the methods a module openly allows can be called; creating, terminating and suspending never reach there. Check that before adding a rule at the gate: what you mean to block may be closed already.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.