# Your First Module

https://dev.wisecp.com/es/your-first-module

Build a working module from an empty directory. The class, its configuration, its language files, and the moment it appears in the admin panel.

## Overview

A module is a directory named after the module. It holds a class file of the same name. It also holds the two files every module has: a configuration array and a language folder. Nothing has to be registered anywhere. The module list reads the directory, so a correctly named directory is a module the moment it exists.

This walkthrough builds a currency rate module, because that is the smallest type with a real job: it answers three methods. The same skeleton is what every other type starts from, and each type adds its own required methods on top of it.

## Prerequisites

- A development installation you can edit and reload. Do not build against an installation you cannot break.
- Write access to `coremio/modules`.
- The conventions the codebase holds itself to, because a module is read and reviewed like core code.

## Structure

Four paths, and the names are not free: the directory, the class file and the class all carry the same name.

```bash
coremio/modules/Currency/AcmeRates/
├── AcmeRates.php      # the class, named after the directory
├── config.php         # returns an array; the module rewrites it when settings are saved
└── lang/
    ├── en.php         # returns an array; 'name' and 'description' are what the panel shows
    └── tr.php
```

- **Module type**: The directory one level up (`Currency` here). It decides which contract the class has to satisfy and where the panel lists it.
- **Module name**: The directory name. Directory, file and class must match exactly, including case.
- **Namespace**: Always `WISECP\Modules\{Type}`. Core classes are then reached with a leading backslash.

## Walkthrough

### Create the Directory

1. Create `coremio/modules/Currency/AcmeRates/`.
2. Create `lang/` inside it.
3. The panel will not list the module yet: it has no class to load.

### Write the Class

1. Create `AcmeRates.php` with the namespace, the class and the three properties every module declares.
2. Load the configuration and the language pack in the constructor, so the rest of the class can read them.
3. Implement the methods the type requires, listed in the contract below.
4. Reload the modules screen; the module is listed with the name from its language file.

### Add Configuration

1. Create `config.php` returning an array of the settings your module needs, with empty or safe defaults.
2. Implement `save_config()` so it merges the submitted values into the array and writes the file back.
3. Write the file through the file manager rather than with a raw write. A configuration file is PHP, and a stale compiled copy keeps serving the old values after a save.
4. Save a setting in the panel and reload; the new value is the one you read back.

### Add Language Files

1. Create `lang/en.php` and `lang/tr.php`, each returning an array.
2. Give both a `name` and a `description`: those two keys are what the module list prints.
3. Reload the list; the module now shows its own name instead of its directory name.

## Reference

### What a Currency Module Must Implement

There is no base class to extend. The contract is the set of methods the core actually calls, and each type has its own. For `Currency` it is three:

```php
// Rate fetch. Called by Money::get_exchange_rates() and by the panel's connection test,
// which checks for it with method_exists first. $to holds the target codes, uppercase.
// Return a map of CODE => rate; anything falsy is treated as a failure.
public function exchange_rates(string $from = '', array $to = []): array|false;

// Settings persistence. The currency settings operation calls it with the submitted
// values for this module only, i.e. $_POST['module_data']['AcmeRates'].
public function save_config(array $data = []): bool;

// The settings markup shown on the currency screen. Called unconditionally on the
// instance, so it must exist even if it returns an empty string.
public function page_settings(): string;
```

- **Field names in page_settings()**: Inputs must be named `module_data[{ModuleName}][{key}]`; that is the shape `save_config()` receives. A differently named field never reaches the module.
- **config['help-link']**: Read by the currency screen and printed next to the module's settings as a link to the provider's own page. Leave it out and no link is shown.
- **lang['name'] · lang['description']**: What the module list prints. Without `name` the list falls back to `config['name']` and then to the directory name.

### Core Calls a Module Makes

```php
// Modules : the factory and the two loaders. All static.
public static function getInstance(string $type, string $name, array $params = []): ?object;
public static function Config($type, $module);                          // cached; requires a prior load
public static function Lang($type, $module, $lang = '');                // loads the file itself
public static function Load($type = '', $name = '', $nominc = false, $status = '');
public static function getName(string $type, string $module): string;
public static function save_log($type = '', $module = '', $action = '', $request = '', $response = '', $processed = '');

// Utility : request and JSON helpers.
public static function HttpRequest($url = '', $params = [], $retry = 0);   // array first argument, see below
public static function jdecode($string = '', $mode = false);              // $mode = true for an associative array
public static function jencode($string = '', $flags = 0): string|false;
public static function array_export($array = [], $options = []);          // ['pwith' => true] wraps it as a PHP file

// FileManager : the write that invalidates the compiled copy.
public static function file_write($file, $data = null, $mode = 'w', $flags = 0);
```

`HttpRequest()` has two shapes; pass an array as the first argument and the rest is ignored:

- **url**: Full address. Build query strings with `urlencode()`; nothing escapes it for you.
- **type**: Method, defaulting to `'GET'`. A body is only sent when the method is not GET.
- **data**: The body. An array is sent as form fields; a string is sent as-is, which is how you post JSON.
- **header**: List of raw header lines: `['Authorization: Bearer ' . $key, 'Content-Type: application/json']`.
- **timeout · connect_timeout · ssl_verify · allow_ipv6**: Defaults are 30s, 10s, verification on and IPv6 off. Leave the last two alone unless the provider forces you.

## Example

The complete class, then the two files it reads. It declares what every module declares and answers the three methods its type asks for. It reports failure the way the rest of the platform does.

```php
namespace WISECP\Modules\Currency;

class AcmeRates
{
    public string $name   = "AcmeRates";
    public ?array $config = null;
    public ?array $lang   = null;

    public function __construct()
    {
        $this->config = \Modules::Config("Currency", $this->name);
        $this->lang   = \Modules::Lang("Currency", $this->name);
    }

    public function save_config(array $data = []): bool
    {
        $merged = array_replace_recursive($this->config ?: [], $data);

        return (bool) \FileManager::file_write(__DIR__ . DS . "config.php", \Utility::array_export($merged, ['pwith' => true]));
    }

    public function exchange_rates(string $from = '', array $to = []): array|false
    {
        $key = (string) ($this->config['apiKey'] ?? '');
        if ($key === '')
            throw new \Exception($this->lang['error-no-key'] ?? 'API key is not set.');

        $response = \Utility::HttpRequest([
            'url'    => 'https://api.example.com/rates?base=' . urlencode($from),
            'type'   => 'GET',
            'header' => ['Authorization: Bearer ' . $key],
        ]);

        // Every call is recorded, so a failing provider can be diagnosed from the panel.
        \Modules::save_log("Currency", $this->name, "exchange", ['from' => $from, 'to' => $to], $response);

        $data = \Utility::jdecode((string) $response, true);
        if (!isset($data['rates']))
            throw new \Exception('Unexpected response from the rate provider.');

        $out = [];
        foreach ($to as $code) $out[$code] = (float) ($data['rates'][$code] ?? 0);

        return $out;
    }

    public function page_settings(): string
    {
        $key = htmlspecialchars((string) ($this->config['apiKey'] ?? ''), ENT_QUOTES);

        // The field name is the contract: this is exactly what save_config() receives.
        return '<div class="row mb-0 align-items-center">'
             . '<label class="col-sm-3 col-form-label fw-semibold">API Key</label>'
             . '<div class="col-sm-9"><input type="text" class="form-control" '
             . 'name="module_data[AcmeRates][apiKey]" value="' . $key . '"></div></div>';
    }
}
```

```php
// config.php : the keys page_settings() prints and save_config() writes back.
return [
    'apiKey'    => '',
    'help-link' => 'https://api.example.com/docs',
];

// lang/en.php : 'name' and 'description' are what the module list shows.
return [
    'name'         => 'Acme Rates',
    'description'  => 'Exchange rates from the Acme provider. An API key is required.',
    'error-no-key' => 'API key is not set.',
];
```

The other half: how the platform reaches the module. Never construct one with `new`, because the factory is what loads the class, fills the configuration and caches the object.

```php
$module = Modules::getInstance("Currency", "AcmeRates");

// Guard on the contract, not on the type: a module is a plain class and may predate a method.
if ($module && method_exists($module, "exchange_rates"))
    $rates = $module->exchange_rates("USD", ["EUR", "TRY"]);

// Only the configuration and the language pack, without instantiating anything.
Modules::Load("Currency", "AcmeRates", true);
$config = Modules::Config("Currency", "AcmeRates");
$label  = Modules::getName("Currency", "AcmeRates");
```

## Pitfalls

> **Report failure by throwing**
> 
> A module signals a problem the same way an operation does: throw, with a message a person can read. The caller catches it and surfaces that message. An `$error` property set alongside `return false` is a leftover from the previous major version. You will see it in older ports, but new code does not use it.

> **A configuration file is compiled PHP**
> 
> Write it through the file manager. A raw write leaves the previously compiled copy in place. The panel then shows the old value after a save that actually succeeded.

> **Inside the module namespace an unqualified core class does not resolve**
> 
> `Utility::jdecode(...)` written in a module file is looked up as `WISECP\Modules\Currency\Utility` and fails at runtime, not at lint time. Prefix core classes with a backslash or import them.

> **Your own helper classes are not modules**
> 
> A class you put under the module's own source folder is ordinary PHP and is constructed with `new`. It is also not autoloaded, so include it before use. The factory is only for the module types the platform knows about.

## Related Articles

- [Module Anatomy](https://dev.wisecp.com/en/module-anatomy)
- [Module Configuration](https://dev.wisecp.com/en/module-configuration)
- [Module Language Files](https://dev.wisecp.com/en/module-language-files)
- [Writing a Currency Module](https://dev.wisecp.com/en/writing-a-currency-module)
