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

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

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

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

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.