# Points d'accroche

Le noyau expose, les modules consomment. Un module **ne doit jamais**
modifier `backoffice/` ou `site/`.

## La règle

Développer un module ne demande aucune modification du noyau. Si ça
arrive, c'est qu'il manque une accroche — et **poser une accroche est
un travail de framework**, pas de module.

Une accroche légitime est **générique et permanente**. Elle ne sait
rien de ce qui viendra s'y brancher.

```php
// Correct : le noyau ignore qui s'y branche
Hooks::render('technique.tabs');

// Faux : le noyau connaît un module
if ($module === 'commerce') { … }
```

## Ce qui existe

### Emplacements de page

Chaque page du back-office et du site expose deux emplacements :

```
page.<slug>.before     au-dessus du contenu
page.<slug>.after      en dessous
```

Par exemple `page.users.before`, `page.home.after`. Une page ajoutée
les obtient automatiquement.

### Emplacements de gabarit

```
backoffice.head                 backoffice.menu.after
backoffice.header.actions       backoffice.content.before / .after
backoffice.footer.scripts       page.actions

site.head                       site.nav.actions
site.body.start                 site.content.before / .after
site.footer.scripts
```

### Emplacements à onglets

```
settings.tabs / settings.panels        Réglages
technique.tabs / technique.panels      Technique
dashboard.before / .after / .stats     Tableau de bord
```

Un module qui déclare un onglet fournit le panneau du même
identifiant.

### Services

```
filter:mailer.config        expéditeur, SMTP
filter:mailer.sending       avant envoi — renvoyer false l'annule
filter:view.data            données d'une vue, avant rendu
filter:router.route         route résolue
filter:router.missing       adresse inconnue, avant le 404
filter:auth.permission      accès à une page
```

### Authentification

```
auth.login              connexion réussie
auth.logout             déconnexion, session encore ouverte
auth.registered         inscription d'un visiteur (site)
```

### Cycle de l'application

```
app.booted              modules chargés, avant toute requête
middleware.before       avant un contrôle d'accès
module.activated        un module vient d'être activé
module.deactivated      un module vient d'être désactivé

filter:api.response     réponse d'API, avant envoi
filter:security.sanitize  nettoyage d'une valeur entrante
```

### Cycle des données

Sur tout modèle qui hérite de `Model` :

```
filter:model.create         données avant insertion
filter:model.update         données avant mise à jour
filter:model.delete         renvoyer false annule la suppression
```

### Transformations et événements

```
filter:backoffice.menu      arbre du menu
filter:dashboard.stats      cartes du tableau de bord
filter:setting.<clé>        valeur d'un réglage

activity.create / .update / .delete
```

### Fichiers

```
filter:media.owners         types de propriétaire de fichiers d'un module
filter:media.referenced     un brouillon est-il encore désigné ? true le garde
```

Règle du cœur (`Media`) : **un fichier appartient à une seule chose et
vit dans son dossier**. L'arborescence de `site/public/uploads/` dit qui
possède quoi :

```
core/                 le cœur : réglages, pages, comptes, brouillons
modules/<clé>/        un module, et lui seul
files/                libre : la page Fichiers
```

Un module déclare ses propriétaires **avec sa clé** ; c'est le cœur qui
place leur dossier sous `modules/<clé>/`. Un module ne peut donc pas
écrire ailleurs que chez lui, ni reprendre un type du cœur (`setting`,
`page`, `user`). Une déclaration sans `module` ou sans `folder` est
ignorée, et signalée une fois dans le journal.

```php
// modules/<clé>/hooks/shared.php — les deux côtés en ont besoin

Hooks::filter('media.owners', function (array $owners) {
    // → uploads/modules/commerce/products/<id>/
    $owners['product'] = ['module' => 'commerce', 'folder' => 'products',
                          'permission' => 'shop-catalog'];

    // Pas de dossier : ses fichiers vivent chez son produit.
    $owners['variant'] = ['resolve' => fn($id) => ['product', productOf($id)]];

    return $owners;
});
```

Pour un fichier PRIVÉ, qui ne doit avoir aucune adresse (pièce envoyée
par un client) : `Media::privatePath('<clé>')` →
`storage/modules/<clé>/`, hors de toute partie publique. Le module en
contrôle l'accès.

## S'y brancher

```php
// modules/<clé>/hooks/backoffice.php

Hooks::on('technique.tabs', function () {
    echo '<button class="dev__tab" data-tab="mon-outil">…</button>';
}, 20);

Hooks::filter('mailer.config', function (array $config, array $ctx) {
    $config['fromName'] = 'Mon site';
    return $config;
}, 10);
```

`hooks/backoffice.php` est chargé côté administration,
`hooks/site.php` côté public.

## Déclarer les siennes

Un module qui expose ses propres accroches les décrit dans
`hooks/catalog.php` :

```php
return [
    'mon-module.avant' => 'Description lisible',
];
```

Elles apparaissent alors dans Technique, avec celles du noyau.

## Il en manque une ?

Dis-le plutôt que de modifier le noyau en douce. Une accroche posée
sert à tous les modules futurs — c'est un investissement, pas une
rustine.
