# 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.