# Service Module Hooks

https://dev.wisecp.com/es/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

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 PHP

```php
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 (`terminate` → `cancel`) 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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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**.

## Related Articles

- [Service Status Hooks](https://dev.wisecp.com/en/service-status-hooks)
- Module Lifecycle Hooks
- [Service Lifecycle Hooks](https://dev.wisecp.com/en/service-lifecycle-hooks)
