# Writing a Hook Listener

https://dev.wisecp.com/es/writing-a-hook-listener

Writing the function that binds to a hook: taking the parameters in order, giving the return its contract asks for.

## Overview

A listener is three things: the **name** you bind to, the **number** that is your place in the queue, the **function** that runs. The hard part is none of those. The hard part is getting the function's **signature and return** right.

Every hook carries its own contract. One hands you the value by reference and waits for you to change it. One prints the text you return. One throws that text as an error and stops the action. One never looks at your return.

Reading the contract wrong **reports nothing**: the listener runs and nothing happens.

## Prerequisites

- [How Hooks Work](https://dev.wisecp.com/en/how-hooks-work) read first.
- The name of the hook you bind to, and its **entry**. The parameters get their meaning there.
- A file for the listener: one under `coremio/hooks`, or your module's `hooks.php`.

## Step by Step

### 1. Read the contract

1. A hook's entry holds **three lines**: *Mechanism*, *Parameters*, *Return contract*. Read all three.
2. **Do not** decide from the word in front of the name. On measuring, part of the `filter` family was called with a copy.
3. The **Ref** column decides. Take a marked parameter with `&` and you can change it.

### 2. Write the signature

1. Take the parameters **in the order the hook gives them**. The names are yours, the order is not.
2. **Leave out** trailing parameters you do not need. Fewer is safe, more drops the listener.
3. Take the parameter you mean to change with `&`.

```php
// filter:invoice.late_fee_amount — the entry's order: &$fee, $invoice, $cycle
Hook::add('filter:invoice.late_fee_amount', 10, function (&$fee, $invoice) {
    if ((int) ($invoice['user_id'] ?? 0) === 1) $fee = 0.0;   // third parameter left out
});
```

### 3. Give the return

1. Do what the entry's *Return contract* line says. The spread below helps, but that line decides.
2. Return `null` where you mean to touch nothing.
3. Where you stop an action, return **the sentence the user will read**.

### 4. Prove it ran

1. Put the file in place and do the work that fires the hook. No restart is needed.
2. Where nothing happened, read the error log. A `Hook execution error` record says the listener ran and threw.
3. A `Hook listener unresolved` record says the class or method name is wrong.
4. An empty log means the listener was never called: the name is wrong, or the file sits in neither swept place.
5. For a definite answer, run the hook in diagnosis mode.

```php
foreach (Hook::runDetailed('action:service.created', $id, $data) as $row) {
    echo $row['source']['file'] . ':' . $row['source']['line'] . "\n";
    if ($row['error'] !== null) echo '  FAILED: ' . $row['error'] . "\n";
}
```

## Reference

The spread below was counted from **the entries of 984 hooks**. It shows what to expect in a family. It is not the truth for any single hook: every row holds a minority that behaves otherwise.

- **action**: On **297** hooks the return is ignored. **4** gather telemetry and keep what you return.
- **filter**: On **186** hooks you **change the value by reference** and the return is not read. On **7** you **return** the changed array; the last non-empty return wins.
- **gate**: On **128** hooks a **non-empty string** stops the action and is thrown as the error. `null` or an empty string lets the flow carry on.
- **ui**: On **329** hooks you return **the HTML to print**; empty returns are skipped. On **12** you change the object handed to you.
- **register**: On **5** hooks you return a registration array. On **4** you widen the array handed to you, on **2** you return a single value.

### The running order

A smaller number runs first. **The same number cannot be taken twice**: a second listener slides to the next free number.

Pick the number against the other listeners. Take a large one to change a value with **the last word**. Take a small one to stop an action **before anyone else**.

## Example

One installation's rules for corporate customers: no late fee, a word to an outside system when a service opens, no deleting a service while the account owes money.

```php
<?php

// A VALUE: zero the late fee by reference.
Hook::add('filter:invoice.late_fee_amount', 10, function (&$fee, $invoice) {
    if (Corporate::has((int) ($invoice['user_id'] ?? 0))) $fee = 0.0;
});

// AN EVENT: a service opened. Our return is not read.
Hook::add('action:service.created', 10, function ($id, $data) {
    if (Corporate::has((int) ($data['owner_id'] ?? 0))) Crm::openedService($id, $data);
});

// PERMISSION: a non-empty string stops the delete; the operator reads this sentence.
Hook::add('gate:service.delete', 10, function ($service) {
    $uid = (int) ($service['owner_id'] ?? 0);
    if (Corporate::has($uid) && Corporate::owes($uid))
        return 'A corporate account with an unpaid invoice keeps its services.';
    return null;
});
```

All three sit in one file under three different contracts. One changes its parameter, one returns nothing, one returns text and cuts the action short.

## Pitfalls

> **The ampersand is wasted where the hook offers none**
> 
> Writing `&$value` is not enough on its own; the value has to have been **sent** by reference. Where the entry does not mark that parameter, your change stays in a local copy. There is no symptom: no error, only no effect.

> **A by-reference hook takes no literal**
> 
> Where **you** open a hook in its by-reference form, every argument has to be a variable. Passing an array literal, a text literal, a function's return or a `??` expression is a **fatal error** and takes the page down. Put context values in a variable first.

> **The class form shares one object**
> 
> Registering a listener as a class method builds that class **once**. Every hook using it shares one object. Work in the constructor then happens **once per request** rather than per hook, and state you keep carries to the other hooks. Use the static form where you do not want that.

> **With equal numbers the registering order decides**
> 
> Giving two listeners the same number does not make them equal; the second **slides** to the next free one. Which slides depends on the order the files load in, and that changes as modules are added. Give listeners whose order matters **numbers of their own**.

> **Do not give a gate work to do**
> 
> Blocking hooks run on **every attempt** and their only job is to say yes or no. Calling a remote service or writing data inside one slows the flow down. Worse, another listener may already have stopped the action, leaving what you wrote as a record of **something that never happened**.

## Related Articles

- [How Hooks Work](https://dev.wisecp.com/en/how-hooks-work)
- [Hook Domains](https://dev.wisecp.com/en/hook-domains)
- [Common Hook Scenarios](https://dev.wisecp.com/en/common-hook-scenarios)
