# How Hooks Work

https://dev.wisecp.com/es/how-hooks-work

The way into the system's flow without touching a core file: a name, a running order and a function to run.

## Overview

A hook is the point where the core stops and asks **"has anyone here got something to say?"** An order is saved and it asks. An invoice total is worked out and it asks. A management screen is about to be drawn and it asks. You leave a function behind, and the core calls it on reaching that point.

What this buys you: behaviour added without editing the core. An edited core file comes back on the first update. A hook listener sits in your own file, and the update leaves it alone. This is also how modules bind themselves to the system.

This installation holds **984 hook points** spread over seventeen domains by subject. The word in front of the name says what it was opened for.

- **action**: Something happened and you are being told. Your return is not read — write to an outside system, keep a record, send word. **301 hooks.**
- **filter**: A value passes through your hands and you can **change it**: an amount, a list, a query, template data. **199 hooks.**
- **gate**: Permission is asked before an action runs and you can stop it by saying **no**. **131 hooks.**
- **ui**: You put your own markup at a named place on a screen. The largest family: **342 hooks.**
- **register**: You make a new capability known to the system: a route, a dashboard piece, a report. **11 hooks.**

## Prerequisites

- A file for your listener to live in: a `.php` file in the installation's own `coremio/hooks` directory, or a module's `hooks.php`.
- A finished installation. No listener loads before the setup wizard is done (see the pitfalls below).
- The **exact name** of the hook you are binding to. A wrong name does nothing at all and reports nothing either.

## Structure

Listener files are read **on their own** and you register them nowhere. Two places are swept and both load together.

```bash
coremio/hooks/*.php                     # the installation's own listeners
coremio/modules/{Type}/{Name}/hooks.php # every module's own listeners
```

Loading happens **when the first hook runs**, not at page start. The files are read once and stay in memory for that request. So adding a listener restarts nothing: you drop the file in and it runs on the next request.

A hook takes as many listeners as you like. They all run **by running order**, and the smaller number goes first.

## Reference

### Registering a listener

```php
// The Hook class — coremio/classes/Hook.php
static function add(string $name, int $priority, callable|array $properties): void;
```

- **$name**: The full name of the hook you bind to. It is not checked, and a wrong name binds silently to nothing.
- **$priority**: The running order, smaller first. **The same number cannot be taken twice**: a second listener slides to the next free number, so the order of registering decides.
- **$properties**: The thing to run. One of four forms (below).

### The four listener forms

```php
// 1 — a closure: the common one
Hook::add('action:order.created', 10, function ($order) {
    Crm::push($order['id']);
});

// 2 — a class method: the class is built ONCE and shared across every hook
Hook::add('filter:invoice.late_fee_amount', 10, ['class' => 'AcmeBilling', 'method' => 'adjust']);

// 3 — a static method: no object is built
Hook::add('gate:order.checkout', 10, ['class' => 'AcmeGuard', 'method::static' => 'allow']);

// 4 — the constructor: with NO method key the class is built and THE OBJECT is the return
Hook::add('register:routes', 10, ['class' => 'AcmeRoutes']);
```

### How the core runs a hook

```php
static function run(string $name, mixed ...$args): array;
static function runRefs(string $name, mixed &...$args): array;
static function runDetailed(string $name, mixed ...$args): array;
```

- **Hook::run()**: Passes a **copy** of the arguments. A change the listener makes to the variable never reaches the caller, and the returns are gathered into an array.
- **Hook::runRefs()**: Passes the arguments **by reference**: a listener writing `&$x` in its signature changes the caller's variable. This is how the `filter` family works.
- **Hook::runDetailed()**: Returns each listener's **source file, line, return and error**. The "who is listening to this" screen in the panel uses it, and it is the right tool for diagnosis too.

What all three share: a listener that throws is **caught, recorded, and the next one runs**. A single broken listener stops neither the core nor the other listeners.

## Example

One file, three listeners, three different jobs: catching an event, changing a value and stopping an action.

```php
<?php

// 1 — AN EVENT: an order was written, tell the outside. Our return is not read.
Hook::add('action:order.created', 10, function ($order) {
    Crm::push((int) $order['id'], $order);
});

// 2 — A VALUE: change the row. With & on the first argument the caller's variable changes.
Hook::add('filter:admin.table.rows', 10, function (&$row, $table) {
    if ($table === 'services') $row['name'] = strtoupper($row['name'] ?? '');
});

// 3 — PERMISSION: returning a non-empty string stops the action and that text reaches the user.
Hook::add('gate:order.checkout', 10, function ($member, $items) {
    if (Blocklist::has((int) ($member['id'] ?? 0)))
        return 'This account cannot order.';
    return null;                    // null or an empty string: the flow carries on
});
```

It ends the moment you drop the file in. There is no registering, no build and no restart; all three run on the next request.

## Pitfalls

> **A wrong name says nothing**
> 
> Binding a listener to a hook name that does not exist **reports nothing**; the listener never runs. Where a listener "does not work", the first place to look is not the code but **the name**. Check it against the hook index rather than writing it from memory.

> **An empty argument is skipped and the rest slide**
> 
> The core binds a listener's parameters **in order** and skips an empty (`null`) argument. The gap does not close: **everything after it slides one to the left** and the second parameter lands where the first was. Never pass an optional context value as empty; pass nothing, or a placeholder such as an empty string or an empty array.

> **The returned array does not line up with the listeners**
> 
> The gathered array carries the **non-empty** returns alone. Where one of three listeners returns empty, the array holds two items and **the first item is no longer the first listener**. Taking "the first result" is safe on hooks with a single listener and nowhere else.

> **One parameter too many drops the listener**
> 
> Writing a three-parameter listener where the hook throws two values makes the call fail. The failure is caught and recorded, and **the listener is skipped**. What you see from outside is "it does not work". Check how many values the listener takes against the hook's entry. Writing **fewer** parameters is safe; writing more is not.

> **The prefix states intent, not mechanism**
> 
> A name starting with `filter:` is **no guarantee** the value arrives by reference. On measuring, part of the `filter` family was being called with a copy. Do not read off its name whether a hook can truly change its value. Read it from the mechanism line in the hook's entry.

> **No listener loads before setup finishes**
> 
> While the setup stamp is empty, **none** of the hook files are read. Do not expect to reach into the setup wizard's flow with a hook; that ground comes before hooks exist.

## Related Articles

- [Writing a Hook Listener](https://dev.wisecp.com/en/writing-a-hook-listener)
- [Hook Domains](https://dev.wisecp.com/en/hook-domains)
- [Common Hook Scenarios](https://dev.wisecp.com/en/common-hook-scenarios)
