# Operations

https://dev.wisecp.com/es/operations

Every change to data goes through an operation. That is why permissions, demo mode, hooks and error formatting are decided once instead of in every screen.

## Overview

An operation is a method that a request can name. When a request carries an operation name, the controller builds no page. It checks the privilege attached to that operation, calls the method, and turns whatever happens into a JSON response.

The method itself is written in a trait, so a controller gains a family of related operations by using one. The controller declares which operations it accepts and what each one requires.

## Structure

- **coremio/operations/**: One trait per family. The controller uses the trait, and its methods become the controller's own.
- **Registration**: The controller lists the operations it accepts, each with the privileges it needs. Registration is what attaches a privilege; it is not what makes a method reachable.
- **The operation object**: Constructed by the dispatcher and handed to your method. It carries the operation name, the registered properties, the demo guard, the hook helper and the response.

### What the Dispatcher Does For You

```php
// operation(): accepted when the name is REGISTERED or simply EXISTS as a method
$isOperation = isset($operations[$name]);
$isMethod    = method_exists($this, $name);
$properties  = $operations[$name] ?? [];

if ($isOperation || $isMethod) return $this->run_operation($name, $properties);

// Neither: modules get the last word, and an array answer becomes the response.
foreach (Hook::run("register:admin.operations", Controllers::$cname, $name) as $hfn)
    if ($hfn && is_array($hfn)) { echo Utility::jencode($hfn); return true; }

// Still nothing: {"status":"error","message":"Undefined operation: <name>"}

// run_operation(), in order:
//   1. privileges from the registration -> Admin::isPrivilege(), else throw
//   2. method_exists($this, $method),                            else throw
//   3. $this->$method(new Operation($name, $properties));
//   4. catch (Exception $e) -> {"status":"error","message":$e->getMessage()}
```

The last step is the one that changes how you write. You do not build error responses; you throw, and the message you throw is what the caller reads.

## Reference

### The Operation Object

```php
public ?string $name = '';               // this operation's registered name
public static ?string $last = '';        // the last operation constructed in this process

public function __construct($name = '', $properties = []);
public function demo(): void;                                        // throws in demo mode
public function hook(string $name = '', array $vars = []): array;    // ['overwrite' => array] | ['output' => mixed] | []
public static function output($response): bool;                      // always returns true
public static function name(): ?string;                              // reads $last, NOT $this->name
```

- **demo()**: Throws when the installation runs in demo mode, so the dispatcher answers with the standard refusal. The first line of every operation that writes.
- **output()**: An array is sent as JSON with a content type header and pretty, unescaped encoding; a non-array is echoed as is. A *falsy* response writes nothing at all and still returns true, so `output([])` answers with an empty body the caller cannot parse. Static, but called through the instance by convention, and returned so the method stops there.
- **hook()**: Fires an extension point and hands back what listeners returned. See the contract below for the accepted names and the shape of the answer.
- **name()**: Static, and it reads the process-wide `$last`, not the instance. Inside an operation the two agree; from a listener that runs later, prefer the name the hook already passes you.

### The Hook Contract

Two names are shorthand for the shared extension points, and anything else is used verbatim. Listeners always receive three arguments: the controller name, the operation name and the variables you passed.

```php
// Inside the operation. "before" => filter:admin.operation.before
//                       "after"  => filter:admin.operation.after
$hook = $operation->hook('before', get_defined_vars());
if ($hook && $hook["overwrite"] ?? []) extract($hook["overwrite"]);   // listener changed your locals
if ($hook && $hook["output"] ?? false) return $operation->output($hook["output"]);

// In a module's hooks.php. The listener decides by returning ONE of three shapes:
Hook::add('filter:admin.operation.before', 1, function ($controller, $operation, $vars) {
    if ($controller !== 'widgets' || $operation !== 'save_widget') return null;

    // 1. refuse:  hook() throws with this message, run_operation turns it into the error JSON
    if (!$vars["name"]) return ['status' => "error", 'message' => "Name is required"];

    // 2. rewrite: these become local variables in the operation, via extract()
    if ($vars["name"] === "x") return ['overwrite_vars' => ['name' => "X"]];

    // 3. answer:  the operation returns this instead of doing the work
    return ['output' => ['status' => "successful", 'id' => 0]];
});
```

- **filter:admin.operation.before**: Fired after input is read and validated, before anything is written. The usual place to refuse or to rewrite the values.
- **filter:admin.operation.after**: Fired once the work is done and the response array is built, so a listener can replace the response the caller sees.
- **register:admin.operations**: The fallback for a name that is neither registered nor a method. A listener returning an array answers the request itself, which is how a module adds an operation to a core screen.
- **filter:api.response**: Fired by `output()` on every array response, by reference, with the operation name as the second argument. The last chance to add or strip a field.

### The Response

Success and failure share one field, so a caller can branch on `status` alone. The error shape is produced for you by the dispatcher. The success shape is whatever you pass; by convention it carries the same field plus what the screen needs.

```json
{ "status": "successful", "id": 42, "message": "Changes saved" }

{ "status": "error", "message": "Name is required" }
```

## Example

```php
namespace WISECP\Operations;

use Exception;
use Filter;
use Language;
use Operation;

trait AdminWidgets
{
    public function save_widget(Operation $operation): bool
    {
        // Demo mode refuses every write; this is always the first line.
        $operation->demo();

        $id   = (int) Filter::init("POST/id", "rnumbers");
        $name = Filter::init("POST/name", "hclear");

        if (!$name) throw new Exception(Language::gc("widgets/error-name-required"));

        $hook = $operation->hook('before', get_defined_vars());
        if ($hook && $hook["overwrite"] ?? []) extract($hook["overwrite"]);
        if ($hook && $hook["output"] ?? false) return $operation->output($hook["output"]);

        $saved = $id
            ? $this->model->update($id, ['name' => $name])
            : $this->model->add(['name' => $name]);

        if (!$saved) throw new Exception(Language::gc("widgets/error-save-failed"));

        $response = ['status' => "successful", 'id' => (int) ($id ?: $saved)];

        $hook = $operation->hook('after', get_defined_vars());
        if ($hook && $hook["overwrite"] ?? []) extract($hook["overwrite"]);
        if ($hook && $hook["output"] ?? false) return $operation->output($hook["output"]);

        return $operation->output($response);
    }
}
```

The other side is an ordinary form post to the controller's own address, with the operation name as a field.

```bash
curl -X POST "$PANEL/widgets" \
     -H "X-Requested-With: XMLHttpRequest" \
     -d "operation=save_widget&id=42&name=Sidebar"

# {"status":"successful","id":42}
# {"status":"error","message":"You don't have privileges to access this operation."}
```

## Pitfalls

> **The demo guard is not optional**
> 
> Without it, a demo installation performs the write and only looks like it refused. It goes first, before any reading or validation, so nothing can slip past it.

> **A method with no registration has no privilege**
> 
> The dispatcher runs a method that merely exists, with an empty property array. An empty privilege list means the check is skipped entirely. Every public method a controller gains from a trait is an entry point. Register it, or do not put it on the controller.

> **Throw translated messages**
> 
> The message you throw is shown to whoever made the request. A literal string means the installation answers everyone in one language.

> **Return the output, do not only call it**
> 
> `output()` writes the response and returns true; it does not stop the method. Anything after an unreturned call still runs and can print a second body after the JSON.

## Related Articles

- [Controllers and Routing](https://dev.wisecp.com/en/controllers-and-routing)
- [Filtering User Input](https://dev.wisecp.com/en/filtering-user-input)
- [Error Handling](https://dev.wisecp.com/en/error-handling)
- [Adding a Custom Operation](https://dev.wisecp.com/en/adding-a-custom-operation)
