# Créer un module

Un module ajoute des fonctions au framework sans modifier un seul de
ses fichiers. C'est le seul mécanisme d'extension : tout ce qui ajoute
quelque chose est un module.

## Structure

```
modules/mon-module/
    module.php                  déclaration — obligatoire
    hooks/backoffice.php        contributions à l'administration
    hooks/site.php              contributions au site public
    hooks/shared.php            ce qui vaut des deux côtés
    Controllers/                chargés automatiquement
    Models/
    Services/
    views/pages/<slug>.php      vues des pages déclarées
    assets/                     servis sous /modules/<clé>/<fichier>
    database/migrations/*.sql   jouées à l'activation
    database/uninstall.sql      jouée à la désinstallation
    bootstrap.php               exécuté au démarrage, facultatif
```

Seul `module.php` est obligatoire. Le reste s'ajoute selon les besoins.

## Déclaration

```php
return [
    'key'         => 'mon-module',
    'name'        => 'Mon module',
    'description' => 'Ce qu'il fait, en une ligne',
    'icon'        => 'bi-puzzle',
    'version'     => '1.0',
    'category'    => 'Contenu',

    'targets'  => ['backoffice', 'site'],
    'requires' => [],

    // Pages créées à l'activation, avec leurs permissions
    'pages' => [
        ['slug' => 'mon-module', 'title' => 'Mon module',
         'icon' => 'bi-puzzle', 'menu' => true],
    ],

    // Réglages semés à l'activation
    'settings' => ['mon_module_option' => 'valeur'],
];
```

Déclarer une page suffit à obtenir son entrée de menu **et** ses
permissions : elles dérivent des pages dans ce framework.

## Points d'accroche

Le framework expose des emplacements, les modules s'y branchent. Trois
façons d'intervenir.

### Afficher

```php
Hooks::on('backoffice.header.actions', function (array $ctx) {
    echo '<a href="…" class="btn btn--ghost btn--sm">…</a>';
}, 30);
```

### Réagir

```php
Hooks::on('activity.delete', function (array $ctx) {
    if ($ctx['type'] === 'media') { /* nettoyer */ }
});
```

`activity.<action>` est émis à chaque création, modification et
suppression du framework — c'est le point de passage qui couvre tout.

### Transformer

```php
Hooks::filter('dashboard.stats', function (array $stats) {
    $stats[] = ['label' => 'Réalisations', 'value' => 12];
    return $stats;
});
```

Chaque module reçoit le résultat du précédent : les transformations se
composent au lieu de s'écraser. Un module qui ne renvoie rien laisse la
valeur inchangée.

## Emplacements du framework

| Nom | Où |
|-----|-----|
| `backoffice.head` | En-tête HTML de l'administration |
| `backoffice.header.actions` | Barre du haut, à gauche du menu utilisateur |
| `backoffice.menu.after` | Sous le menu latéral |
| `backoffice.content.before` | Au-dessus du contenu de page |
| `backoffice.content.after` | Sous le contenu de page |
| `backoffice.footer.scripts` | Fin de document |
| `site.head` | En-tête HTML du site |
| `site.body.start` | Ouverture du corps |
| `site.content.before` | Au-dessus du contenu |
| `site.content.after` | Sous le contenu |
| `site.footer.scripts` | Fin de document |
| `page.actions` | Boutons d'action d'une page — le contexte porte son nom |
| `dashboard.before` / `.after` | Autour de la grille du tableau de bord |
| `settings.tabs` / `.panels` | Onglet de réglages et son contenu |
| `activity.<action>` | Événement : `create`, `update`, `delete`… |

## Transformations disponibles

| Nom | Valeur transformée |
|-----|--------------------|
| `dashboard.stats` | Compteurs du tableau de bord |
| `backoffice.menu` | Arbre du menu latéral — réordonner, masquer, ajouter |
| `setting.<clé>` | Valeur d'un réglage, sans l'écrire en base |

`backoffice.menu` reçoit l'arbre **après** calcul des permissions :
réintroduire une entrée à ce stade la rendrait visible sans droit.

Le contexte de `content.before` et `content.after` porte la page en
cours : une contribution peut donc s'afficher partout ou nulle part.

## Priorité

`50` par défaut, ce qui laisse de la place des deux côtés. Petit
d'abord.

## Isolation

Un module qui lève une exception est **journalisé sans emporter la
page** : elle resterait blanche, et il serait impossible d'aller le
désactiver.

## Exemple complet

`modules/realisations/` est un module fonctionnel et autonome :

- sa page de gestion avec son entrée de menu et ses permissions
- ses tables, avec migration et script de désinstallation
- un raccourci dans l'en-tête, visible seulement pour qui peut écrire
- un bandeau sur sa page quand des brouillons attendent
- un nettoyage automatique quand un média est supprimé
- un compteur ajouté au tableau de bord
- un encart sur le site, hors de sa propre page
- un bouton d'export sur la page Médias, qu'il ne possède pas
- son entrée de menu remontée sous le tableau de bord

Aucun fichier du framework n'a été modifié pour qu'il fonctionne.

## Déclarer vos propres accroches

Un module peut à son tour exposer des accroches — c'est ce que fait
commerce avec le panier et la fiche produit.

Décrivez-les dans `hooks/catalog.php` :

```php
return [
    'mon-module.liste.after' => 'Sous la liste des éléments',
    'filter:mon-module.tri'  => 'Changer l'ordre d'affichage',
];
```

**Jamais dans le noyau.** `Hooks::catalog()` agrège celles du framework
et celles des modules actifs : le noyau n'a pas à connaître ce qui
n'existe qu'en option, et un module désinstallé retire ses lignes de
lui-même.

Sans cette règle, chaque nouveau module exigerait une modification du
framework rien que pour se documenter.

## Voir ce qui existe

**Technique › Modules** liste toutes les accroches — celles du
framework et celles des modules actifs — avec leur description, et
signale celles qu'un module utilise déjà.
