Files
slim-blog/docs/ARCHITECTURE.md
2026-03-16 15:37:57 +01:00

224 lines
8.5 KiB
Markdown

# Architecture
Slim Blog suit désormais une organisation verticale légère par domaine, avec quatre zones récurrentes :
- `Domain` : règles métier simples, politiques, objets de valeur utilitaires
- `Application` : orchestration des cas d'usage
- `Infrastructure` : PDO, filesystem local, services techniques concrets
- `Http` : contrôleurs et adaptation requête/réponse
Cette structure garde les mêmes fonctionnalités qu'au départ, mais réduit la dette de transition : routes, conteneur DI et tests pointent désormais directement vers les implémentations finales.
## Vue d'ensemble
| Domaine | Rôle principal | Dépendances métier |
|-------------|----------------|--------------------|
| `Auth/` | Connexion, sessions, réinitialisation de mot de passe | `User/` |
| `Category/` | Catégories éditoriales | — |
| `Media/` | Upload, stockage local, usage dans les articles | `Post/` pour le comptage d'usage |
| `Post/` | Articles, recherche, RSS | `Category/` en présentation |
| `User/` | Comptes, rôles, création/suppression | — |
| `Shared/` | Infrastructure transverse | — |
## Structure cible d'un domaine
```text
src/MonDomaine/
├── Application/
│ └── MonDomaineApplicationService.php
├── Domain/
│ └── RegleOuPolitique.php
├── Http/
│ └── MonDomaineController.php
├── Infrastructure/
│ └── PdoMonDomaineRepository.php
├── MonEntite.php
├── MonDomaineRepositoryInterface.php
└── MonDomaineServiceInterface.php
```
Principes appliqués dans le projet :
1. Les contrôleurs HTTP dépendent d'interfaces de service (`*ServiceInterface`).
2. Les services applicatifs dépendent d'interfaces de repository et, si nécessaire, de ports techniques.
3. Les implémentations concrètes vivent en `Infrastructure/` et sont câblées dans `config/container.php`.
4. `Shared/` reste réservé au transverse : bootstrap, pagination, session, mail, sanitation HTML, utilitaires communs.
## Dépendances inter-domaines
Les dépendances entre domaines métier restent limitées et unidirectionnelles :
- **`Auth/ → User/`** : `AuthApplicationService` et `PasswordResetApplicationService` lisent les comptes via `UserRepositoryInterface`.
- **`Post/ → Category/`** : `Post\Http\PostController` injecte `CategoryServiceInterface` pour alimenter les formulaires et filtres.
- **`Media/ → Post/`** : `MediaApplicationService` utilise `PostRepositoryInterface` pour vérifier l'usage d'un média avant suppression.
Aucun domaine ne dépend d'une implémentation concrète d'un autre domaine.
## Bindings DI principaux
| Interface | Implémentation |
|-----------|----------------|
| `AuthServiceInterface` | `Auth\Application\AuthApplicationService` |
| `PasswordResetServiceInterface` | `Auth\Application\PasswordResetApplicationService` |
| `LoginAttemptRepositoryInterface` | `Auth\Infrastructure\PdoLoginAttemptRepository` |
| `PasswordResetRepositoryInterface` | `Auth\Infrastructure\PdoPasswordResetRepository` |
| `CategoryServiceInterface` | `Category\Application\CategoryApplicationService` |
| `CategoryRepositoryInterface` | `Category\Infrastructure\PdoCategoryRepository` |
| `MediaServiceInterface` | `Media\Application\MediaApplicationService` |
| `MediaRepositoryInterface` | `Media\Infrastructure\PdoMediaRepository` |
| `MediaStorageInterface` | `Media\Infrastructure\LocalMediaStorage` |
| `PostServiceInterface` | `Post\Application\PostApplicationService` |
| `PostRepositoryInterface` | `Post\Infrastructure\PdoPostRepository` |
| `UserServiceInterface` | `User\Application\UserApplicationService` |
| `UserRepositoryInterface` | `User\Infrastructure\PdoUserRepository` |
## Schéma de base de données
```text
users
├── id INTEGER PK AUTOINCREMENT
├── username TEXT UNIQUE NOT NULL
├── email TEXT UNIQUE NOT NULL
├── password_hash TEXT NOT NULL
├── role TEXT NOT NULL DEFAULT 'user'
└── created_at DATETIME
categories
├── id INTEGER PK AUTOINCREMENT
├── name TEXT UNIQUE NOT NULL
└── slug TEXT UNIQUE NOT NULL
posts
├── id INTEGER PK AUTOINCREMENT
├── title TEXT NOT NULL
├── content TEXT NOT NULL
├── slug TEXT UNIQUE NOT NULL
├── author_id INTEGER → users(id) ON DELETE SET NULL
├── category_id INTEGER → categories(id) ON DELETE SET NULL
├── created_at DATETIME
└── updated_at DATETIME
media
├── id INTEGER PK AUTOINCREMENT
├── filename TEXT NOT NULL
├── url TEXT NOT NULL
├── hash TEXT NOT NULL
├── user_id INTEGER → users(id) ON DELETE SET NULL
└── created_at DATETIME
password_resets
├── id INTEGER PK AUTOINCREMENT
├── user_id INTEGER → users(id) ON DELETE CASCADE
├── token_hash TEXT UNIQUE NOT NULL
├── expires_at DATETIME NOT NULL
├── used_at DATETIME DEFAULT NULL
└── created_at DATETIME
login_attempts
├── ip TEXT PRIMARY KEY
├── attempts INTEGER NOT NULL DEFAULT 0
├── locked_until TEXT DEFAULT NULL
└── updated_at TEXT NOT NULL
```
### Index utiles
| Index | Colonne(s) | Usage |
|-------|------------|-------|
| `idx_posts_author_id` | `posts(author_id)` | listes admin / recherche auteur |
| `idx_media_user_id` | `media(user_id)` | galerie média par utilisateur |
| `idx_media_hash_user_id` | `media(hash, user_id)` | déduplication par utilisateur |
## Flux principaux
### Création / mise à jour d'un article
1. `Post\Http\PostController` lit la requête HTTP.
2. `PostApplicationService` valide, sanitise et génère un slug unique.
3. `PdoPostRepository` persiste en base.
4. Twig rend la réponse ou le contrôleur redirige avec un flash.
### Upload d'un média
1. `Media\Http\MediaController` reçoit le fichier et l'utilisateur courant.
2. `MediaApplicationService` valide le type et la taille, prépare l'upload.
3. `LocalMediaStorage` convertit l'image en WebP et écrit le fichier.
4. `PdoMediaRepository` persiste l'enregistrement.
5. La déduplication se fait par couple `(hash, user_id)`.
### Réinitialisation de mot de passe
1. `PasswordResetController` applique le rate limit par IP.
2. `PasswordResetApplicationService` invalide les anciens tokens actifs, crée un nouveau token hashé et envoie l'e-mail.
3. La consommation du token est atomique via `PdoPasswordResetRepository`.
## Arborescence actuelle
```text
src/
├── Auth/
│ ├── Application/
│ ├── Domain/
│ ├── Http/
│ ├── Infrastructure/
│ ├── Middleware/
│ ├── AuthServiceInterface.php
│ ├── LoginAttemptRepositoryInterface.php
│ ├── PasswordResetRepositoryInterface.php
│ └── PasswordResetServiceInterface.php
├── Category/
│ ├── Application/
│ ├── Domain/
│ ├── Http/
│ ├── Infrastructure/
│ ├── Category.php
│ ├── CategoryRepositoryInterface.php
│ └── CategoryServiceInterface.php
├── Media/
│ ├── Application/
│ ├── Domain/
│ ├── Exception/
│ ├── Http/
│ ├── Infrastructure/
│ ├── Media.php
│ ├── MediaRepositoryInterface.php
│ └── MediaServiceInterface.php
├── Post/
│ ├── Application/
│ ├── Domain/
│ ├── Http/
│ ├── Infrastructure/
│ ├── Post.php
│ ├── PostRepositoryInterface.php
│ └── PostServiceInterface.php
├── Shared/
│ ├── Database/
│ ├── Exception/
│ ├── Extension/
│ ├── Html/
│ ├── Http/
│ ├── Mail/
│ ├── Pagination/
│ ├── Util/
│ ├── Bootstrap.php
│ ├── Config.php
│ └── Routes.php
└── User/
├── Application/
├── Domain/
├── Exception/
├── Http/
├── Infrastructure/
├── User.php
├── UserRepositoryInterface.php
└── UserServiceInterface.php
```
## Règles de maintenance
- Ajouter une nouvelle fonctionnalité dans le domaine concerné avant de créer un nouveau dossier partagé.
- Préférer une nouvelle classe d'application ciblée à une extension d'un contrôleur trop gros.
- Garder les interfaces aux frontières utiles : repository, session, mail, storage.
- Éviter les wrappers de compatibilité temporaires une fois la migration terminée.
- Valider chaque lot avec PHPUnit et PHPStan avant de passer au suivant.