diff --git a/README.md b/README.md index f0d1bba..1fa5e99 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ L'ensemble de la documentation technique et des guides pratiques est organisé d - **[02 - Cycle de Vie d'une Requête (Bullet Tracer)](documentation/Architecture/02-cycle-de-vie-requete.md)** : Traçage pas-à-pas d'une requête HTTP depuis la route jusqu'à la sérialisation JSON. - **[03 - Exécution, Transactions & Événements](documentation/Architecture/03-execution-et-transactions.md)** : `UsecaseBuilder`, `LaravelExecutor`, exécution transactionnelle et dispatch sécurisé post-commit (`TransactionalDomainEventDispatcher`). - **[04 - Guide Nouveau Cas d'Usage](documentation/Architecture/04-guide-nouveau-usecase.md)** : Checklist pas-à-pas pour créer une nouvelle fonctionnalité de bout en bout. +- **[05 - Temps, Horloge PSR-20 & Base de Données](documentation/Architecture/05-temps-et-base-de-donnees.md)** : Contrat PSR-20 `Clock`, Façade `Utc`, tests avec `FrozenClock`, standardisation `DATETIME` et dérivation `created_at` depuis UUIDv7. ### 📐 Conventions & Standards - **[Conventions de Tests](documentation/Conventions/tests.md)** : Principes F.I.R.S.T., structure GWT, pyramide de tests, **Test Data Builders** et **Object Mothers**. diff --git a/documentation/Architecture/01-architecture-globale.md b/documentation/Architecture/01-architecture-globale.md index 15cca8a..ab68fd5 100644 --- a/documentation/Architecture/01-architecture-globale.md +++ b/documentation/Architecture/01-architecture-globale.md @@ -72,15 +72,19 @@ Arcadia API combine 3 paradigmes éprouvés pour créer une base de code hauteme - **Contenu** : - `Adapters/` : Implémentations Eloquent des Ports (`EloquentEmployeeDirectory`, `EloquentBrandRepository`). - `Database/Models/` : Modèles Eloquent anémiques (mapping BDD uniquement). + - `Database/Concerns/` : Traits de persistance (`HasUuidV7CreatedAt`). + - `Casts/` : Casts Eloquent bidirectionnels (`UtcDateTimeCast`, `UuidCast`, etc.). - `Http/Middleware/` : Middlewares techniques (HMAC, Auth). - - `Providers/` : Configuration des bindings Laravel (`DomainServiceProvider`, `SecurityServiceProvider`). + - `Providers/` : Configuration des bindings Laravel (`DomainServiceProvider`, `SecurityServiceProvider`, `ConfigServiceProvider`). - `Usecase/LaravelExecutor.php` : Exécuteur transactionnel et logging PSR-3. --- ### 5. `Shared\` - Le Socle DDD Transverse -- **Dépendance** : Zéro dépendance externe. -- **Contenu** : Interfaces de base (`Entity`, `EntityId`, `Usecase`, `Input`, `Output`, `Presenter`, `DomainEventDispatcher`, `TransactionalDomainEventDispatcher`, `UsecaseBuilder`, `Paginated`, `Pagination`). +- **Dépendance** : Zéro dépendance externe (hors PSR-20). +- **Contenu** : + - Socle DDD (`Entity`, `EntityId`, `Usecase`, `Input`, `Output`, `Presenter`, `DomainEventDispatcher`, `TransactionalDomainEventDispatcher`, `UsecaseBuilder`, `Paginated`, `Pagination`). + - Gestion du temps (`Shared\Time\` : `Clock` PSR-20, `SystemClock`, `SystemUtcClock`, `FrozenClock`, Façade `Utc`). --- diff --git a/documentation/Architecture/05-temps-et-base-de-donnees.md b/documentation/Architecture/05-temps-et-base-de-donnees.md new file mode 100644 index 0000000..5f37a81 --- /dev/null +++ b/documentation/Architecture/05-temps-et-base-de-donnees.md @@ -0,0 +1,167 @@ +# Gestion du Temps, Horloge PSR-20 & Persistance des Dates - Arcadia API + +Ce document détaille la stratégie de gestion temporelle, le contrat d'horloge PSR-20 (`ClockInterface`), la normalisation **UTC / RFC 3339** et la politique de stockage des dates en base de données dans **Arcadia API**. + +--- + +## 📑 Sommaire + +- [1. Principes Fondamentaux](#1-principes-fondamentaux) +- [2. L'Horloge PSR-20 & La Façade `Utc`](#2-lhorloge-psr-20--la-façade-utc) +- [3. Tests & Voyage Temporel (`FrozenClock`)](#3-tests--voyage-temporel-frozenclock) +- [4. Normalisation des Sorties d'API (RFC 3339)](#4-normalisation-des-sorties-dapi-rfc-3339) +- [5. Persistance en Base de Données (`DATETIME` vs `TIMESTAMP`)](#5-persistance-en-base-de-données-datetime-vs-timestamp) +- [6. Dérivation Automatique de `created_at` depuis UUIDv7](#6-dérivation-automatique-de-created_at-depuis-uuidv7) + +--- + +## 1. Principes Fondamentaux + +1. **UTC partout en interne** : Tout calcul métier, comparaison et stockage temporel s'effectue strictement en UTC (`\DateTimeImmutable` avec `DateTimeZone('UTC')`). +2. **Immutabilité stricte** : `\DateTime` (mutable) est **strictement interdit** dans tout le code source (contrôlé par les tests d'architecture Pest). Seul `\DateTimeImmutable` est autorisé. +3. **Contrat d'Horloge Injectable (PSR-20)** : Tout composant ayant besoin de l'instant présent doit consommer le contrat `Shared\Time\Clock` (ou la façade `Utc`) pour éliminer les appels directs non déterministes à `now()`, `time()` ou `new DateTimeImmutable()`. +4. **Pas de bug de l'an 2038** : Le type SQL `DATETIME` est la norme absolue sur toutes les tables. + +--- + +## 2. L'Horloge PSR-20 & La Façade `Utc` + +### A. Le Contrat `Shared\Time\Clock` +Arcadia API implémente le standard officiel **PSR-20 (Clock)** via l'interface `Shared\Time\Clock` : + +```php +namespace Shared\Time; + +use Psr\Clock\ClockInterface; + +interface Clock extends ClockInterface +{ + #[\Override] + public function now(): \DateTimeImmutable; + + public function timezone(): \DateTimeZone; + + public function utcNow(): \DateTimeImmutable; +} +``` + +- **`SystemClock`** : Horloge système initialisée dynamiquement avec le fuseau configuré (`$appConfig->timezone->toDateTimeZone()`). +- **`SystemUtcClock`** : Horloge système garantie 100% UTC. + +### B. La Façade Statique `Utc` +Pour les besoins transverses et les formattages sans injection lourde, la façade statique `Shared\Time\Utc` fournit des helpers sécurisés : + +```php +use Shared\Time\Utc; + +// Obtenir l'instant présent en UTC +$now = Utc::now(); + +// Formater en RFC 3339 (ex: 2026-08-21T14:30:00Z) +$formatted = Utc::toRfc3339($dateTime); + +// Parser une date brute et garantir son fuseau UTC +$parsed = Utc::parse('2026-08-21 14:30:00'); +``` + +--- + +## 3. Tests & Voyage Temporel (`FrozenClock`) + +Dans les tests unitaires et d'intégration, il est interdit de dépendre de l'horloge réelle de la machine hôte. Utilisez `FrozenClock` ou `Utc::freeze()` : + +```php +use Shared\Time\FrozenClock; +use Shared\Time\Utc; + +it('valide l expiration du token avec precision', function (): void { + // GIVEN - Figer le temps + $fixedDate = new \DateTimeImmutable('2026-08-21 12:00:00 UTC'); + $clock = new FrozenClock($fixedDate); + + // OU via la façade Utc + Utc::freeze($fixedDate); + + // WHEN + $validator = new TimestampValidatorImpl($clock); + $isValid = $validator->validate('2026-08-21T12:02:00Z'); + + // THEN + expect($isValid)->toBeTrue(); + + // Nettoyage + Utc::reset(); +}); +``` + +--- + +## 4. Normalisation des Sorties d'API (RFC 3339) + +Toutes les dates exposées par l'API (dans les DTOs sous `Presentation\Api\V1\Dto\`) doivent être sérialisées au format **RFC 3339** (avec le suffixe `Z` pour UTC) : + +```php +final readonly class BrandDto implements ApiResponseDto +{ + public function __construct(private BrandView $view) {} + + public function toArray(): array + { + return [ + 'id' => $this->view->id, + 'name' => $this->view->name, + 'created_at' => Utc::toRfc3339($this->view->createdAt), + ]; + } +} +``` + +--- + +## 5. Persistance en Base de Données (`DATETIME` vs `TIMESTAMP`) + +### Pourquoi `DATETIME` sur 100% des Tables ? + +1. **Pérennité (Pas de bug 2038)** : `TIMESTAMP` est stocké sur un entier 32-bit signé qui déborde le **19 janvier 2038**. `DATETIME` est valide jusqu'à l'an 9999. +2. **Déterminisme & Absence de Magie SQL** : `TIMESTAMP` applique des conversions implicites en fonction de la variable `@@session.time_zone` de MySQL/MariaDB. `DATETIME` stocke la valeur brute exacte transmise par l'application. +3. **Cast Eloquent Bidirectionnel (`UtcDateTimeCast`)** : + +```php +// Infra/Casts/UtcDateTimeCast.php +final class UtcDateTimeCast implements CastsAttributes +{ + // Convertit toute valeur DB en DateTimeImmutable UTC à la lecture + // Formate en 'Y-m-d H:i:s' UTC à l'écriture +} +``` + +--- + +## 6. Dérivation Automatique de `created_at` depuis UUIDv7 + +Les clés primaires de nos agrégats (`brands`, `api_clients`, `client_audit_logs`) utilisent des **UUIDv7**. +Comme un UUIDv7 encode de manière standardisée l'instant de création dans ses 48 premiers bits : + +### Le Trait `HasUuidV7CreatedAt` (`Infra\Database\Concerns\`) + +Au lieu de colonnes calculées complexes dont les dialectes varient entre MySQL, MariaDB et PostgreSQL, nous utilisons le trait Eloquent **`HasUuidV7CreatedAt`** : + +```php +namespace Infra\Database\Models; + +use Illuminate\Database\Eloquent\Model; +use Infra\Database\Concerns\HasUuidV7CreatedAt; + +final class Brand extends Model +{ + use HasUuidV7CreatedAt; + + // Lors du 'creating', le trait extrait automatiquement + // les millisecondes de l'UUIDv7 ($model->id) et renseigne 'created_at' +} +``` + +### Règle de Schéma +- Les colonnes `created_at` sont déclarées **`dateTime('created_at')` non-nullables** dans les migrations. +- L'intégrité est totale : si un modèle est inséré sans UUIDv7 valide ou sans `created_at`, la base de données rejette la requête. +- Le Domaine reste **100% pur** : l'entité de domaine dérive son `createdAt()` directement de son `BrandId` (Value Object) sans dépendance d'infrastructure.