Service Module Hooks
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
Runs while a service is built from an order item, before the record is written.
type, product_id, amount, status, module, options, metrics.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
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.
options is what the module object is built from.terminate → cancel) happens after this hook, so a new value is resolved too.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;
});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
Runs before the module method is called. This gate stands on calls from the panel.
create, suspend, cancel and the like.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
Runs after the module method ran — whether it worked or not.
service, instance (the module object), action, result, error. Unlike the other hooks the parameters do not arrive separately; they all sit inside this one.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
Runs before the module's return is handled by the core. All five arguments arrive by reference.
false, or something else. This is the one you usually change.null. Writing here tells the core "this failed".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
Runs where a customer calls a module action from their own panel. This gate is separate from the panel one.
sso_panel_login) or the handle_ form.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
Runs after the module method the customer called has run.
tool_action, tool_table, sso_panel_login, or a key the module opened.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
Runs after the configuration screen the module offers was built.
actions and fields. You can add fields, drop them or make them read-only.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
Runs after the service's panel password changed.
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
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.
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
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.
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.
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.
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.
Related Articles
- Service Status Hooks
- Module Lifecycle Hooks
- Service Lifecycle Hooks
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.