# Créer un module

## En bref

Un module ajoute une fonctionnalité complète — tables, pages, routes,
permissions — sans modifier le noyau. Ses tables ne sont créées qu'à
l'activation : un site qui ne l'active pas n'en porte aucune trace.

## Démarrer

1. Copiez ce dossier et renommez-le. La clé du manifeste doit être
   **exactement** le nom du dossier.
2. Ouvrez `module.php`, renseignez nom, description, icône.
3. Écrivez vos migrations dans `database/migrations/`.
4. Écrivez `database/uninstall.sql` — **sans lui, la désinstallation est
   refusée** : le framework ne devine pas quelles tables vous appartiennent.
5. Activez depuis **Technique › Modules**.

## Ce que le framework fait pour vous

| Vous déclarez | Vous obtenez |
|---------------|--------------|
| Un dossier avec `module.php` | Détection automatique |
| Des migrations | Exécution à l'activation, suivi sous `<clé>:<fichier>` |
| Des pages dans le manifeste | Pages en base **et permissions par rôle** |
| `routes/site.php`, `routes/backoffice.php` | Routes ajoutées après celles du noyau |
| `Controllers/`, `Models/`, `Services/` | Chargement automatique des classes |
| `views/` | Vues remplaçables via `custom/modules/<clé>/` |

## Trois règles

**Les migrations ne se modifient jamais après diffusion.** Elles sont
suivies par nom de fichier : une migration déjà exécutée ne sera pas
rejouée. Une correction se fait par une nouvelle migration.

**`bootstrap.php` s'exécute à chaque requête.** On y déclare, on n'y
exécute pas. Aucune requête en base, aucun traitement lourd.

**Un module ne peut pas détourner une route du noyau.** Ses routes sont
ajoutées après : le premier motif qui correspond gagne.

## Barre d'enregistrement

Deux variantes selon la page :

| Classe | Usage |
|--------|-------|
| `settings-actions` | Page courte, sans onglets — la barre suit le contenu |
| `floating-actions` | Page longue ou à onglets — barre flottante centrée |

Elle reprend le fond de la barre latérale : elle appartient au châssis de
l'interface, pas au contenu. Les boutons `btn--ghost` et `btn--outline` y
sont automatiquement adaptés au fond sombre.

La barre flottante **n'apparaît qu'après modification** : ajoutez
`floating-actions--visible` quand un champ change, retirez-la après
enregistrement.

```js
$('#mes-actions').addClass('floating-actions--visible');
```

Affichée en permanence, elle occuperait le bas de l'écran sans rien avoir à
dire. Pensez à une marge basse d'environ 80 pixels sur le conteneur, sinon
elle recouvre le dernier champ.

**Les actions destructrices n'y ont pas leur place** : à côté du bouton
d'enregistrement, on les clique par erreur — le geste est le même, la
conséquence non.

## Migrations

Un fichier peut contenir **plusieurs instructions** : elles sont découpées et
exécutées une par une. Les points-virgules à l'intérieur des chaînes et des
commentaires sont protégés.

Les migrations doivent **tolérer d'être rejouées** : le bouton « Réparer »
les relance quand des tables déclarées manquent. Table, colonne ou index déjà
présent est ignoré ; toute autre erreur interrompt.

### Remplacer un index

L'ordre compte. Un index dont la première colonne porte une clé étrangère est
utilisé par MySQL pour vérifier la contrainte : il refuse de le supprimer tant
qu'aucun autre ne peut prendre le relais.

```
Erreur 1553 : Cannot drop index 'uq_link', needed in a foreign key
```

**Créez le nouvel index avant de retirer l'ancien** :

```sql
ALTER TABLE t ADD UNIQUE KEY uq_nouveau (a, b, c);
ALTER TABLE t DROP INDEX uq_ancien;
```

## Largeur des pages

Deux largeurs, jamais adaptatives — une largeur qui varie selon le contenu
déplace les repères d'une page à l'autre.

| Classe | Usage |
|--------|-------|
| `page--narrow` | 900 px centrés — formulaires, réglages, fiches |
| `page--compact` | 640 px centrés — formulaire simple, connexion |
| *aucune* | pleine largeur — listes, tableaux, grilles |

**Deux façons de l'appliquer.**

Sur le **conteneur racine** quand toute la page est un formulaire :
l'en-tête est centré avec le reste, et les barres d'onglets échappent
d'office au centrage.

```html
<div class="page--narrow">
    <div class="page-header">…</div>
    <div class="dev__tabs">…</div>   <!-- reste pleine largeur -->
    <div class="card">…</div>
</div>
```

Sur des **panneaux précis** quand la page mélange formulaires et listes —
une fiche à onglets, par exemple. L'en-tête et les onglets restent alors
pleine largeur, comme sur une page de liste.

**Le critère n'est pas la page, c'est son contenu.** Ce qu'on lit et remplit
de haut en bas se centre : étiré sur un écran large, l'œil parcourt trop de
distance entre l'étiquette et son champ. Ce qu'on parcourt en balayant reste
large : plus de colonnes visibles, moins de troncatures.

Une page à onglets peut mélanger les deux — la classe se pose sur le
panneau, pas sur la page.

## Position dans le menu

Le menu du back-office suit trois zones :

| Positions | Zone | Contenu |
|-----------|------|---------|
| 1 à 99 | Travail | Dashboard, Statistiques, Utilisateurs, Messagerie |
| 100 à 999 | **Modules** | Vos entrées, insérées automatiquement |
| 1000 et + | Administration | Paramètres, Dev |

Un module s'insère **juste avant** le premier dossier d'administration : il
apparaît donc parmi les fonctions métier, jamais relégué en bas.

**Une seule page au menu** donne une entrée simple. **Plusieurs** donnent un
dossier déroulant portant le nom du module — un dossier pour une page unique
ajouterait un clic sans rien organiser.

Les positions sont espacées de dix : insérer un module entre deux autres ne
demande pas de tout renuméroter.

## Cycle de vie

```
Activer      → migrations exécutées, module en route
Désactiver   → code arrêté, DONNÉES CONSERVÉES
Désinstaller → tables supprimées, irréversible
```

Un module dont un autre dépend ne peut être ni désactivé ni désinstallé
tant que le dépendant est actif.

## Événements

Réagir à ce qui se passe ailleurs, sans que les modules se connaissent :

```php
// Dans bootstrap.php
Events::on('order.paid', function (array $payload) {
    // $payload['order_id'], $payload['total']
});

// Refuser une action
Events::on('cart.item.adding', function (array $payload) {
    if ($payload['quantity'] > 10) {
        return ['cancel' => true, 'reason' => 'Dix maximum'];
    }
    return $payload;
});
```

Une erreur dans un abonné est journalisée sans interrompre les autres : un
module défaillant ne doit pas empêcher l'enregistrement d'une commande déjà
payée.
