8.5 KiB
8.5 KiB
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 utilitairesApplication: orchestration des cas d'usageInfrastructure: PDO, filesystem local, services techniques concretsHttp: 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
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 :
- Les contrôleurs HTTP dépendent d'interfaces de service (
*ServiceInterface). - Les services applicatifs dépendent d'interfaces de repository et, si nécessaire, de ports techniques.
- Les implémentations concrètes vivent en
Infrastructure/et sont câblées dansconfig/container.php. 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/:AuthApplicationServiceetPasswordResetApplicationServicelisent les comptes viaUserRepositoryInterface.Post/ → Category/:Post\Http\PostControllerinjecteCategoryServiceInterfacepour alimenter les formulaires et filtres.Media/ → Post/:MediaApplicationServiceutilisePostRepositoryInterfacepour 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
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
Post\Http\PostControllerlit la requête HTTP.PostApplicationServicevalide, sanitise et génère un slug unique.PdoPostRepositorypersiste en base.- Twig rend la réponse ou le contrôleur redirige avec un flash.
Upload d'un média
Media\Http\MediaControllerreçoit le fichier et l'utilisateur courant.MediaApplicationServicevalide le type et la taille, prépare l'upload.LocalMediaStorageconvertit l'image en WebP et écrit le fichier.PdoMediaRepositorypersiste l'enregistrement.- La déduplication se fait par couple
(hash, user_id).
Réinitialisation de mot de passe
PasswordResetControllerapplique le rate limit par IP.PasswordResetApplicationServiceinvalide les anciens tokens actifs, crée un nouveau token hashé et envoie l'e-mail.- La consommation du token est atomique via
PdoPasswordResetRepository.
Arborescence actuelle
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.