Une bibliothèque PHP pour la création de structures de domaine type-safety, immutables et robustes, spécialement conçue pour l'architecture hexagonale et le Domain-Driven Design (DDD).
- À propos
- Installation
- Concepts fondamentaux
- Systèmes transverses
- Utilisation
- Bonnes pratiques
- Support
Domain Structures est une bibliothèque PHP qui fournit une base solide pour construire des applications avec une architecture propre et type-safe. Elle implémente les patterns fondamentaux du Domain-Driven Design :
- Value Objects : Concepts métier auto-validants
- Records : Structures de données internes immutables
- Data DTO : Objets de transfert pour les réponses API
- Typed Collections : Collections type-safe
- Hydratation automatique : Création d'objets depuis n'importe quelle source
- Normalisation : Export vers des structures simples (JSON, base de données)
"Rien n'est primitif, tout est concept"
Dans une application bien architecturée, on ne manipule jamais de types primitifs directement. Chaque donnée est représentée par un concept explicite :
| Au lieu de... | Utilisez... |
|---|---|
int $id |
UserId $id |
string $email |
EmailAddress $email |
float $price |
Money $price |
array $products |
ProductCollection $products |
composer require andydefer/domain-structuresPrérequis :
- PHP 8.1 ou supérieur
- Extension JSON activée
Les Value Objects représentent des concepts métier avec leur propre comportement et validation.
use AndyDefer\DomainStructures\Abstracts\AbstractValueObject;
final class EmailAddress extends AbstractValueObject
{
private function __construct(public readonly string $value) {}
public static function from(mixed $source): static
{
if (!filter_var($source, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email");
}
return new self($source);
}
public function getDomain(): string { /* ... */ }
public function isGmail(): bool { /* ... */ }
}
// Utilisation
$email = EmailAddress::from('john@example.com');
echo $email->getDomain(); // 'example.com'Caractéristiques :
- ✅ Immutable
- ✅ Auto-validant
- ✅ Comportement métier
- ✅ Pas d'identité propre
- ❌ Pas d'effets de bord
👉 Documentation complète des Value Objects
2. Records
Les Records sont des structures de données internes pour la communication entre les couches de l'application.
use AndyDefer\DomainStructures\Abstracts\AbstractRecord;
use AndyDefer\DomainStructures\Traits\Hydratable;
final class UserRecord extends AbstractRecord
{
use Hydratable;
public function __construct(
public readonly ?int $id,
public readonly string $name,
public readonly EmailAddress $email,
public readonly UserRole $role,
public readonly Iso8601DateTime $createdAt,
) {}
}
// Hydratation automatique depuis une source externe
$user = UserRecord::from([
'id' => 123,
'name' => 'John Doe',
'email' => 'john@example.com',
'role' => 'admin',
'created_at' => '2024-01-01T12:00:00+00:00'
]);Caractéristiques :
- ✅ Immutable
- ✅ Hydratation automatique
- ✅ Normalisation en
snake_case - ✅ Support JSON
- ❌ Pas de logique métier
👉 Documentation complète des Records
3. Data DTO
Les Data DTO sont des objets de transfert exclusivement pour les réponses API.
use AndyDefer\DomainStructures\Abstracts\AbstractData;
final class UserData extends AbstractData
{
public function __construct(
public readonly UserId $id,
public readonly PersonName $name,
public readonly EmailAddress $email,
public readonly Iso8601DateTime $createdAt,
public readonly UserRole $role,
public readonly ProductDataCollection $purchasedProducts,
) {}
}
// Réponse API
$userData = UserData::from($userRecord);
return response()->json($userData); // camelCase pour le clientCaractéristiques :
- ✅ Exclusivement pour les réponses API
- ✅ Normalisation en
camelCase - ✅ Collections typées concrètes
- ❌ Aucun type primitif autorisé
👉 Documentation complète des Data DTO
Les Typed Collections remplacent les tableaux bruts par des collections type-safe.
// ❌ Tableau brut
public readonly array $items; // On ne sait pas ce qu'il contient
// ✅ Collection typée
public readonly ProductRecordCollection $items; // TypedCollection<ProductRecord>
// Création d'une collection spécialisée
final class ProductRecordCollection extends TypedCollection
{
public function __construct()
{
parent::__construct(ProductRecord::class);
}
public function getFeatured(): self
{
return $this->filter(fn(ProductRecord $product) => $product->isFeatured);
}
}
// Utilisation
$products = new ProductRecordCollection();
$products->add($product1, $product2, $product3);
$featured = $products->getFeatured();Collections utilitaires prédéfinies :
StringTypedCollectionIntTypedCollectionFloatTypedCollectionBoolTypedCollectionNumberTypedCollection(int|float)
👉 Documentation complète des Typed Collections
5. DataObject
DataObject est un normalisateur d'accès aux données qui sert de pont entre les sources externes et le système d'hydratation.
use AndyDefer\DomainStructures\Utils\DataObject;
// Source externe (snake_case)
$apiData = [
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe'
];
// Normalisation
$normalized = DataObject::from($apiData);
// Accès indifférent camelCase/snake_case
echo $normalized->userId; // 123
echo $normalized->first_name; // "John"
echo $normalized->lastName; // "Doe"
// Transformation immuable
$updated = $normalized->with('email', 'john@example.com');Caractéristiques :
- ✅ Normalisation de l'accès (camelCase/snake_case)
- ✅ Conversion récursive des tableaux
- ✅ Méthodes
with(),merge(),without() - ✅ Intégration avec Hydratable
- ❌ Pas d'immutabilité stricte
👉 Documentation complète de DataObject
Le trait Hydratable analyse le constructeur d'une classe et l'hydrate automatiquement depuis n'importe quelle source.
use AndyDefer\DomainStructures\Traits\Hydratable;
final class ProductRecord extends AbstractRecord
{
use Hydratable;
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly float $price
) {}
}
// Une méthode pour toutes les sources
$product = ProductRecord::from($array); // Tableau
$product = ProductRecord::from($object); // Objet
$product = ProductRecord::fromJson($json); // JSON (recommandé)
// Collection d'objets
$products = ProductRecord::collect($sources);Support :
- ✅ Types scalaires (int, float, string, bool)
- ✅ Enums (BackedEnum)
- ✅ Unions types
- ✅ Transformable (hydratation récursive)
- ✅ Valeurs par défaut
- ✅ Nullabilité
👉 Documentation complète de Hydratable
Le système de normalisation convertit récursivement les objets complexes en structures simples (tableaux, scalaires).
use AndyDefer\DomainStructures\Normalizers\NormalizerChain;
$user = new UserRecord(id: 123, name: 'John', email: EmailAddress::from('john@example.com'));
// Normalisation automatique
$normalized = NormalizerChain::get()->normalize($user);
// Résultat : ['id' => 123, 'name' => 'John', 'email' => 'john@example.com']
// JSON direct
$json = json_encode($normalized);Normaliseurs disponibles :
NullNormalizer→ nullScalarNormalizer→ scalairesEnumNormalizer→ valeur ou nomRecordNormalizer→ tableau (camelCase → snake_case)ValueObjectNormalizer→ valeur bruteDataNormalizer→ tableau (conserve camelCase)TypedCollectionNormalizer→ tableau indexéDataObjectNormalizer→ tableau associatifArrayNormalizer→ récursif
👉 Documentation complète de la Normalisation
// 1. Définir les Value Objects
final class EmailAddress extends AbstractValueObject { /* ... */ }
final class Iso8601DateTime extends AbstractValueObject { /* ... */ }
// 2. Définir l'Enum
enum UserRole: string { case ADMIN = 'admin'; case USER = 'user'; }
// 3. Définir le Record
final class UserRecord extends AbstractRecord
{
use Hydratable;
public function __construct(
public readonly ?int $id,
public readonly string $name,
public readonly EmailAddress $email,
public readonly UserRole $role,
public readonly Iso8601DateTime $createdAt,
) {}
}
// 4. Définir la collection spécialisée
final class UserRecordCollection extends TypedCollection
{
public function __construct()
{
parent::__construct(UserRecord::class);
}
public function getAdmins(): self
{
return $this->filter(fn(UserRecord $user) => $user->role === UserRole::ADMIN);
}
}
// 5. Utilisation dans un Repository
class UserRepository
{
public function find(int $id): ?UserRecord
{
$row = $this->db->fetchAssoc('SELECT * FROM users WHERE id = ?', [$id]);
return $row ? UserRecord::from($row) : null;
}
public function findAll(): UserRecordCollection
{
$rows = $this->db->fetchAllAssoc('SELECT * FROM users');
return UserRecord::collect($rows, UserRecordCollection::class);
}
}
// 6. Utilisation dans un Controller
class UserController
{
public function show(int $id): JsonResponse
{
$user = $this->userRepository->find($id);
// Normalisation automatique pour l'API
return response()->json(NormalizerChain::get()->normalize($user));
}
}// ✅ BON - Validation centralisée
final class EmailAddress extends AbstractValueObject { /* validation */ }
// ❌ MAUVAIS - Validation dispersée
$email = $_POST['email'];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { /* ... */ }// ✅ BON - Uniquement pour la communication interne
public function find(int $id): UserRecord
// ❌ MAUVAIS - Dans une réponse API (utilisez Data DTO)
return response()->json($userRecord);// ✅ BON - Pour les réponses API
return response()->json($userData);
// ❌ MAUVAIS - Avec des types primitifs
public readonly int $id; // Interdit ! Utilisez UserId $id// ✅ BON - Collection spécialisée
public readonly ProductRecordCollection $products;
// ❌ MAUVAIS - TypedCollection générique
public readonly TypedCollection $products;// ✅ BON - fromJson() pour le JSON
$user = UserRecord::fromJson($jsonResponse);
// ✅ BON - from() pour les tableaux
$user = UserRecord::from($array);
// ❌ MAUVAIS - from() avec JSON (traité comme string)
$user = UserRecord::from($jsonResponse);| Concept | Documentation |
|---|---|
| Value Objects | VALUE_OBJECTS.md |
| Records | RECORDS.md |
| Data DTO | DATA.md |
| Typed Collections | TYPED_COLLECTIONS.md |
| DataObject | DATA_OBJECTS.md |
| Hydratation | HYDRATABLE.md |
| Normalisation | NORMALIZATION.md |
Pour toute question ou suggestion :
- Issues : GitHub Issues
- Documentation : Consultez les fichiers dans le dossier
/concepts - Contact : Équipe de développement
MIT License - Copyright (c) 2024 Andy Defer
Domain Structures vous permet de construire des applications PHP avec :
✅ Type-safety : Tous les types sont explicites et validés
✅ Immutabilité : Aucune modification accidentelle
✅ Hydratation automatique : Création d'objets depuis n'importe quelle source
✅ Normalisation : Export vers JSON, base de données, cache
✅ Collections typées : Remplacement type-safe des tableaux
✅ Architecture propre : Séparation claire des responsabilités
Commencez dès maintenant :
composer require andydefer/domain-structures