andydefer/php-records

A typed data structure package that brings type safety to internal application communication in PHP.

Maintainers

Package info

github.com/andydefer/php-records

pkg:composer/andydefer/php-records

Statistics

Installs: 13

Dependents: 2

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-05-22 08:43 UTC

This package is auto-updated.

Last update: 2026-05-22 08:47:31 UTC


README

Une bibliothèque de structures de données typées pour la communication interne entre les couches de votre application.

PHP Version License

Introduction

PHP Records est une bibliothèque qui remplace les tableaux bruts (array) par des structures de données typées, immuables et prévisibles.

Le problème

// ❌ On ne sait pas ce qu'il y a dans ce tableau
function updateUser(array $data): void
{
    // $data['name'] ? $data['email'] ? $data['role'] ?
    // Personne ne le sait vraiment.
}

La solution

// ✅ On sait exactement ce qu'on reçoit
function updateUser(UserRecord $user): void
{
    // $user->name, $user->email, $user->role
    // Le compilateur guide le développeur.
}
Problème des tableaux Solution avec Record
On ne sait pas ce qu'ils contiennent Propriétés typées explicites
Pas de validation à l'ajout Types garantis à la construction
Documentation implicite Auto-documenté par le code
Refactoring dangereux Le compilateur guide les modifications

Installation

composer require andydefer/php-records

Prérequis

  • PHP 8.1 ou supérieur
  • Aucune dépendance externe obligatoire

Concept fondamental

Qu'est-ce qu'un Record ?

Un Record est une structure de données typée utilisée pour la communication interne entre les couches de l'application (Services, Repositories, Tasks, Workers).

Record → Remplace les tableaux bruts par des structures typées et immutables

Philosophie

Un Record est un sac de données typé, sans aucune logique métier. Il ne fait que transporter des données d'un point A à un point B.

Séparation des responsabilités

Composant Rôle
Record Communication interne (Services, Repositories)
Service Logique métier
Data/Resource Réponse API (si nécessaire dans votre architecture)

Les Records

Définition d'un Record

use AndyDefer\Records\AbstractRecord;

final class UserRecord extends AbstractRecord
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly UserRole $role,
        public readonly TypedCollection $tags = new TypedCollection('string'),
    ) {}
}

Règles fondamentales

Règle Explication
Étendre AbstractRecord Tous les Records doivent hériter de la classe abstraite
Nommage {Description}Record Ex: UserRecord, PaymentResultRecord
Propriétés public La sérialisation automatique utilise la réflexion
readonly recommandé Immuabilité garantie
Pas de logique métier Ni isValid(), ni save(), ni autre méthode métier

Types autorisés dans un Record

Type Exemple Notes
int public readonly int $id Scalaire
string public readonly string $name Scalaire
float public readonly float $price Scalaire
bool public readonly bool $isActive Scalaire
null public readonly ?string $value Nullable
Enum public readonly UserRole $role Backed enum recommandé
Record public readonly AddressRecord $address Record imbriqué
TypedCollection public readonly TypedCollection $items Collection typée

Types à éviter (ou à convertir avant)

Type Alternative Pourquoi
array brut TypedCollection Perte d'information sur le contenu
Model (Eloquent) UserRecord, DoctorRecord Contient de la logique et des relations
Collection TypedCollection Non typée
Carbon / DateTime string ISO 8601 Contient des comportements
final class GoodRecord extends AbstractRecord
{
    public function __construct(
        public readonly TypedCollection $items,   // ✅ TypedCollection<ItemRecord>
        public readonly int $userId,              // ✅ int
        public readonly string $createdAt,        // ✅ string ISO
    ) {}
}

Le Record optionnel : EmptyRecord

Pour les cas où un paramètre Record est optionnel, utilisez EmptyRecord plutôt que null :

use AndyDefer\Records\EmptyRecord;

final class FindByRecord extends AbstractRecord
{
    public function __construct(
        public readonly Recordable $filters = new EmptyRecord(),
        public readonly ?int $limit = 100,
    ) {}
}

// Utilisation - pas de condition ternaire !
$filtersArray = $record->filters->toArray(); // [] si EmptyRecord
Avec null Avec EmptyRecord
$filters?->toArray() ?? [] $filters->toArray()
Condition ternaire partout Pas de condition
Risque d'erreur Type-safe garanti

Sérialisation automatique

AbstractRecord fournit trois méthodes de sérialisation :

$record = new UserRecord(
    name: 'John Doe',
    email: 'john@example.com',
    role: UserRole::ADMIN,
    createdAt: '2024-01-15T14:30:00Z',
);

// Insertion en base (conserve les null)
DB::table('users')->insert($record->toArray());

// Update (exclut les champs null)
DB::table('users')->where('id', 1)->update($record->toDatabase());

// Envoi à une API externe
Http::post('https://api.external.com/users', $record->toJson());

Normalisation automatique

Type d'entrée Sortie
Record imbriqué array via toArray()
TypedCollection array typé
BackedEnum Valeur scalaire ($enum->value)
PureEnum Nom de l'enum ($enum->name)
DateTimeInterface Y-m-d\TH:i:s\Z
null null (conservé)

Conversion snake_case

Toutes les clés sont automatiquement converties en snake_case :

// Propriété en camelCase dans le Record
public readonly string $emailVerifiedAt;

// Devient 'email_verified_at' dans le tableau
$record->toArray(); // ['email_verified_at' => '2024-01-15T14:30:00Z']

Les TypedCollection

Définition

TypedCollection est une collection type-safe qui remplace les tableaux bruts. Elle garantit que tous les éléments qu'elle contient sont du type déclaré à la construction.

use AndyDefer\Records\Collections\TypedCollection;

// ✅ Collection de strings
$tags = new TypedCollection('string');
$tags->add('developer', 'laravel', 'php');

Pourquoi remplacer les tableaux ?

Problème des tableaux Solution avec TypedCollection
On ne sait pas ce qu'ils contiennent Type explicite (TypedCollection<string>)
Pas de validation à l'ajout Validation automatique
Modification dangereuse Type-safe garanti
Pas de méthodes utilitaires Nombreuses méthodes disponibles

Types supportés

Type Description Exemple
'int' Entier new TypedCollection('int')
'string' Chaîne new TypedCollection('string')
'float' Décimal new TypedCollection('float')
'bool' Booléen new TypedCollection('bool')
'null' Null new TypedCollection('string', 'null')
Record::class Record new TypedCollection(UserRecord::class)
TypedCollection::class Collection imbriquée new TypedCollection(TypedCollection::class)
stdClass::class Objet simple new TypedCollection(stdClass::class)

Types multiples

// Collection acceptant plusieurs types scalaires
$mixed = new TypedCollection('int', 'float', 'string');
$mixed->add(42, 3.14, 'text');

// Collection acceptant Records et scalaires
$items = new TypedCollection(ProductRecord::class, 'string');
$items->add(new ProductRecord(name: 'Laptop'), 'Description');

Règle : Record vs Collection

Un Record représente un ÉLÉMENT UNIQUE. Une collection d'éléments utilise TypedCollection.

Situation Type à utiliser
Un seul utilisateur UserRecord $user
Plusieurs utilisateurs TypedCollection $users
final class DashboardDataRecord extends AbstractRecord
{
    public function __construct(
        public readonly UserRecord $currentUser,
        public readonly TypedCollection $recentOrders,  // TypedCollection<OrderRecord>
        public readonly TypedCollection $tags,          // TypedCollection<string>
    ) {}
}

Création d'une collection

// Collection de strings
$tags = new TypedCollection('string');
$tags->add('developer', 'laravel', 'php');

// Collection d'entiers
$ids = new TypedCollection('int');
$ids->add(1, 2, 3, 4, 5);

// Collection de Records
$products = new TypedCollection(ProductRecord::class);
$products->add(new ProductRecord(name: 'Laptop', price: 999));

// Collection de collections (imbriquée)
$nested = new TypedCollection(TypedCollection::class);
$nested->add($tags, $ids);

Méthodes de base

Méthode Description Exemple
add(...$items) Ajoute des éléments $tags->add('a', 'b', 'c')
toArray(): array Retourne tous les éléments $tags->toArray()
count(): int Nombre d'éléments $tags->count()
isEmpty(): bool Collection vide ? $tags->isEmpty()
isNotEmpty(): bool Collection non vide ? $tags->isNotEmpty()
getAllowedTypes(): array Types autorisés $tags->getAllowedTypes()
firstItem(): mixed Premier élément $tags->firstItem()
first(int $limit): static N premiers éléments $tags->first(3)
lastItem(): mixed Dernier élément $tags->lastItem()
last(int $limit): static N derniers éléments $tags->last(3)

Transformation et requêtes

Méthode Description Exemple
every(Closure): bool Tous les éléments satisfont ? $collection->every(fn($i) => $i > 0)
some(Closure): bool Un élément satisfait ? $collection->some(fn($i) => $i > 10)
map(Closure): static Transforme chaque élément $tags->map(fn($t) => strtoupper($t))
filter(Closure): static Filtre les éléments $tags->filter(fn($t) => strlen($t) > 3)
reject(Closure): static Rejette les éléments $tags->reject(fn($t) => strlen($t) > 3)
each(Closure): static Action sur chaque élément $tags->each(fn($t) => echo $t)
sort(int): static Trie les éléments $numbers->sort()
`sortBy(Closure string, bool): static` Trie par clé/fonction
reverse(): static Inverse l'ordre $collection->reverse()
shuffle(): static Mélange aléatoirement $collection->shuffle()

Calculs

Méthode Description Exemple
`sum(?Closure): int float` Somme
avg(?Closure): ?float Moyenne $numbers->avg()
max(?Closure): mixed Valeur max $numbers->max()
min(?Closure): mixed Valeur min $numbers->min()

Filtrage par type

Méthode Description Exemple
ofType(string): static Filtrer par type $collection->ofType('string')
exceptType(string): static Exclure un type $collection->exceptType('int')
records(): static Filtrer les Records $collection->records()
scalars(): static Filtrer les scalaires $collection->scalars()
ofRecord(string): static Filtrer par classe Record $collection->ofRecord(UserRecord::class)
anyRecord(): static Tous les Records $collection->anyRecord()
getTypes(): static Types distincts présents $collection->getTypes()

Recherche et présence

Méthode Description Exemple
where(string, mixed): static Filtrer par propriété $products->where('price', 100)
whereNotNull(string): static Propriété non nulle $products->whereNotNull('price')
whereNull(string): static Propriété nulle $products->whereNull('price')
contains(mixed): bool Élément existe ? $tags->contains('laravel')
containsType(string): bool Type présent ? $collection->containsType('int')
isOnlyType(string): bool Tous du même type ? $collection->isOnlyType('int')

Slicing et pagination

Méthode Description Exemple
take(int): static N premiers $collection->take(10)
skip(int): static Ignorer N premiers $collection->skip(5)
slice(int, ?int): static Extraire une plage $collection->slice(2, 3)
nth(int, int): static Un élément sur N $collection->nth(2)
values(): static Réindexer les clés $filtered->values()

Manipulation avancée

Méthode Description Exemple
unique(?Closure): static Supprimer doublons $collection->unique()
merge(self): static Fusionner $c1->merge($c2)
intersect(self): static Éléments communs $c1->intersect($c2)
diff(self): static Éléments uniques $c1->diff($c2)
flatMap(Closure): static Aplatir $nested->flatMap(fn($i) => $i)
filterNull(): static Supprimer null $collection->filterNull()
random(int): static Éléments aléatoires $collection->random(3)

Validation et assertions

Méthode Description
isHomogeneous(): bool Tous les éléments du même type ?
isHeterogeneous(): bool Types différents ?
assertAllOfType(string): self Vérifie que tous sont d'un type
assertNotEmpty(): self Vérifie non vide
assertContainsType(string): self Vérifie qu'un type est présent
assertAllImplement(string): self Vérifie l'implémentation
assertScalar(): self Vérifie que tous sont scalaires
assertRecords(): self Vérifie que tous sont des Records
validate(Closure): self Validation personnalisée

Les collections utilitaires

Le package fournit des collections pré-typées pour les cas d'usage les plus courants.

StringTypedCollection

Collection spécialisée pour les chaînes de caractères.

use AndyDefer\Records\Collections\Utility\StringTypedCollection;

$strings = new StringTypedCollection();
$strings->add('  HELLO  ', 'world', 'PHP', '', '  test  ');

Méthodes disponibles

Méthode Description Exemple
toLowercase(): self Convertit en minuscules $strings->toLowercase()
toUppercase(): self Convertit en majuscules $strings->toUppercase()
containsSubstring(string): self Filtre par sous-chaîne $strings->containsSubstring('ell')
startsWith(string): self Filtre par préfixe $strings->startsWith('he')
endsWith(string): self Filtre par suffixe $strings->endsWith('lo')
filterEmpty(): self Supprime les chaînes vides $strings->filterEmpty()
trim(string): self Supprime les espaces $strings->trim()
truncate(int, string): self Limite la longueur $strings->truncate(5, '...')
matchingRegex(string): self Filtre par regex $strings->matchingRegex('/^\d+$/')
join(string): string Joint toutes les chaînes $strings->join(', ')
lengths(): TypedCollection<int> Longueurs des chaînes $strings->lengths()
pad(int, string, int): self Padde les chaînes $strings->pad(10, '-')
`replace(string array, string array): self`
firstCharacter(): self Premier caractère $strings->firstCharacter()
lastCharacter(): self Dernier caractère $strings->lastCharacter()
substring(int, ?int): self Extrait une sous-chaîne $strings->substring(0, 3)
countMatchingRegex(string): int Compte les regex $strings->countMatchingRegex('/\d/')
hasMatchingRegex(string): bool Vérifie si match $strings->hasMatchingRegex('/\d/')
uniqueCaseInsensitive(): self Valeurs uniques (insensible) $strings->uniqueCaseInsensitive()
sortCaseInsensitive(bool): self Tri insensible $strings->sortCaseInsensitive()
removeWhitespace(): self Supprime les espaces $strings->removeWhitespace()
slugify(): self Convertit en slug URL $strings->slugify()
wrap(string, ?string): self Encadre les chaînes $strings->wrap('[', ']')
removePrefix(string): self Supprime un préfixe $strings->removePrefix('pre_')
removeSuffix(string): self Supprime un suffixe $strings->removeSuffix('_suf')

Exemples d'utilisation

$strings = new StringTypedCollection();
$strings->add('Hello World!', '  PHP 8  ', 'test@example.com');

// Transformations
$lowercase = $strings->toLowercase(); // ['hello world!', '  php 8  ', 'test@example.com']
$trimmed = $strings->trim(); // ['Hello World!', 'PHP 8', 'test@example.com']
$slugified = $strings->slugify(); // ['hello-world', 'php-8', 'test-example-com']

// Filtrage
$emails = $strings->matchingRegex('/^[^@]+@[^@]+\.[^@]+$/'); // ['test@example.com']
$startsHello = $strings->startsWith('Hello'); // ['Hello World!']

// Manipulation
$wrapped = $strings->wrap('**'); // ['**Hello World!**', '**  PHP 8  **', '**test@example.com**']
$joined = $strings->join(', '); // 'Hello World!,   PHP 8  , test@example.com'

// Suppression de suffixe
$withSuffix = new StringTypedCollection();
$withSuffix->add('user_suffix', 'admin_suffix');
$withoutSuffix = $withSuffix->removeSuffix('_suffix'); // ['user', 'admin']

IntTypedCollection

Collection spécialisée pour les entiers.

use AndyDefer\Records\Collections\Utility\IntTypedCollection;

$numbers = new IntTypedCollection();
$numbers->add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Méthodes disponibles

Méthode Description Exemple
even(): self Nombres pairs $numbers->even()[2, 4, 6, 8, 10]
odd(): self Nombres impairs $numbers->odd()[1, 3, 5, 7, 9]
zero(): self Zéros $numbers->zero()
nonNegative(): self Non négatifs $numbers->nonNegative()
median(): float Médiane $numbers->median()5.5

Exemples d'utilisation

$numbers = new IntTypedCollection();
$numbers->add(10, 23, 5, 8, 15, 42, 7);

$evenNumbers = $numbers->even(); // [10, 8, 42]
$oddNumbers = $numbers->odd(); // [23, 5, 15, 7]
$median = $numbers->median(); // 10.0 (après tri: [5,7,8,10,15,23,42])
$positive = $numbers->nonNegative(); // Tous les nombres (aucun négatif)

FloatTypedCollection

Collection spécialisée pour les nombres décimaux.

use AndyDefer\Records\Collections\Utility\FloatTypedCollection;

$floats = new FloatTypedCollection();
$floats->add(1.234, 2.567, 3.891);

Méthodes disponibles

Méthode Description Exemple
round(int): self Arrondit à une précision $floats->round(2)[1.23, 2.57, 3.89]
ceil(): self Entier supérieur $floats->ceil()[2.0, 3.0, 4.0]
floor(): self Entier inférieur $floats->floor()[1.0, 2.0, 3.0]
format(int): self Arrondit (alias de round) $floats->format(1)[1.2, 2.6, 3.9]

BoolTypedCollection

Collection spécialisée pour les booléens.

use AndyDefer\Records\Collections\Utility\BoolTypedCollection;

$bools = new BoolTypedCollection();
$bools->add(true, false, true, false, true);

Méthodes disponibles

Méthode Description Exemple
trueOnly(): self Uniquement true $bools->trueOnly()[true, true, true]
falseOnly(): self Uniquement false $bools->falseOnly()[false, false]
countTrue(): int Nombre de true $bools->countTrue()3
countFalse(): int Nombre de false $bools->countFalse()2
allTrue(): bool Tous true ? $bools->allTrue()false
allFalse(): bool Tous false ? $bools->allFalse()false
anyTrue(): bool Au moins un true ? $bools->anyTrue()true
anyFalse(): bool Au moins un false ? $bools->anyFalse()true

NumberTypedCollection

Collection pour les nombres mixtes (int + float).

use AndyDefer\Records\Collections\Utility\NumberTypedCollection;

$numbers = new NumberTypedCollection();
$numbers->add(1, 2.5, 3, 4.7, 5);

Méthodes disponibles

Méthode Description Exemple
positive(): self Nombres positifs (> 0) Hérité de AbstractNumberTypedCollection
negative(): self Nombres négatifs (< 0) Hérité de AbstractNumberTypedCollection
between(int|float, int|float): self Intervalle Hérité de AbstractNumberTypedCollection
average(): float Moyenne Hérité de AbstractNumberTypedCollection
zero(): self Zéros (0 ou 0.0) $numbers->zero()
nonNegative(): self Non négatifs (>= 0) $numbers->nonNegative()
areAllIntegers(): bool Tous entiers ? $numbers->areAllIntegers()false
hasAnyFloat(): bool Au moins un float ? $numbers->hasAnyFloat()true
toFloats(): FloatTypedCollection Convertit en floats $numbers->toFloats()[1.0, 2.5, 3.0, 4.7, 5.0]
toIntegers(): IntTypedCollection Convertit en ints $numbers->toIntegers()[1, 2, 3, 4, 5]
separateTypes(): array Sépare ints et floats $numbers->separateTypes()

Exemples d'utilisation

$numbers = new NumberTypedCollection();
$numbers->add(5, 3.14, 0, -2, 7.5, 0.0);

$positive = $numbers->positive(); // [5, 3.14, 7.5]
$zero = $numbers->zero(); // [0, 0.0]
$nonNegative = $numbers->nonNegative(); // [5, 3.14, 0, 7.5, 0.0]

$allInts = $numbers->areAllIntegers(); // false
$hasFloat = $numbers->hasAnyFloat(); // true

$floats = $numbers->toFloats(); // FloatTypedCollection avec [5.0, 3.14, 0.0, -2.0, 7.5, 0.0]
$ints = $numbers->toIntegers(); // IntTypedCollection avec [5, 3, 0, -2, 7, 0]

$separated = $numbers->separateTypes();
$integers = $separated['integers']; // IntTypedCollection avec [5, 0, -2, 0]
$floatValues = $separated['floats']; // FloatTypedCollection avec [3.14, 7.5]

AbstractNumberTypedCollection

Classe de base pour les collections numériques.

use AndyDefer\Records\Collections\Utility\AbstractNumberTypedCollection;

// Méthodes disponibles dans IntTypedCollection, FloatTypedCollection et NumberTypedCollection

Méthodes statiques

Méthode Description Exemple
range(start, end, step): static Génère une séquence IntTypedCollection::range(1, 10, 2)[1, 3, 5, 7, 9]
// Génération de séquences
$evenNumbers = IntTypedCollection::range(2, 20, 2); // [2, 4, 6, ..., 20]
$descending = IntTypedCollection::range(10, 1, -1); // [10, 9, 8, ..., 1]
$floats = FloatTypedCollection::range(0, 1, 0.25); // [0, 0.25, 0.5, 0.75, 1.0]

Création de collections personnalisées

use AndyDefer\Records\Collections\TypedCollection;
use App\Records\ProductRecord;

final class ProductCollection extends TypedCollection
{
    public function __construct()
    {
        parent::__construct(ProductRecord::class);
    }
    
    public function getTotalPrice(): float
    {
        return $this->sum(fn($product) => $product->price);
    }
    
    public function getInStock(): self
    {
        return $this->filter(fn($product) => $product->stock > 0);
    }
    
    public function filterByCategory(string $category): self
    {
        return $this->filter(fn($product) => $product->category === $category);
    }
    
    public function getFeatured(): self
    {
        return $this->filter(fn($product) => $product->isFeatured === true);
    }
}

// Utilisation
$products = new ProductCollection();
$products->add(
    new ProductRecord(name: 'Laptop', price: 999, stock: 5, category: 'electronics', isFeatured: true),
    new ProductRecord(name: 'Mouse', price: 29, stock: 0, category: 'electronics', isFeatured: false),
    new ProductRecord(name: 'Book', price: 19, stock: 10, category: 'books', isFeatured: true),
);

$totalValue = $products->getTotalPrice();  // 1047
$availableProducts = $products->getInStock();  // Laptop et Book
$electronics = $products->filterByCategory('electronics');  // Laptop et Mouse
$featured = $products->getFeatured(); // Laptop et Book

L'interface Recordable

namespace AndyDefer\Records;

interface Recordable
{
    public function toArray(): array;
    public function toDatabase(): array;
    public function toJson(): string;
}

Tous les Records implémentent automatiquement cette interface via AbstractRecord.

Utilisation dans une signature

// Accepter n'importe quel Record
function processRecord(Recordable $record): void
{
    $data = $record->toArray();
    // ...
}

// Ou spécifiquement un EmptyRecord pour les options
function findUsers(Recordable $filters = new EmptyRecord(): array
{
    $filtersArray = $filters->toArray(); // [] si EmptyRecord
    // ...
}

Le trait Enumable

Le trait Enumable ajoute des méthodes utilitaires à vos énumérations PHP 8.1+.

use AndyDefer\Records\Traits\Enumable;

enum UserRole: string
{
    use Enumable;
    
    case ADMIN = 'admin';
    case USER = 'user';
    case GUEST = 'guest';
}

Méthodes disponibles

Méthode Description Exemple
values(): array Retourne toutes les valeurs UserRole::values()['admin', 'user', 'guest']
names(): array Retourne tous les noms UserRole::names()['ADMIN', 'USER', 'GUEST']
typesInOrder(): array Retourne les cas dans l'ordre UserRole::typesInOrder()
isValid(string|int): bool Vérifie si une valeur existe UserRole::isValid('admin')true
fromValue(string|int): ?self Récupère un cas par sa valeur UserRole::fromValue('admin')UserRole::ADMIN

Exemple complet

enum TestUserStatus
{
    use Enumable;
    
    case ACTIVE;
    case INACTIVE;
    case SUSPENDED;
}

// Pure enum (non-backed)
TestUserStatus::values();    // ['ACTIVE', 'INACTIVE', 'SUSPENDED']
TestUserStatus::names();     // ['ACTIVE', 'INACTIVE', 'SUSPENDED']
TestUserStatus::isValid('ACTIVE');  // true
TestUserStatus::fromValue('ACTIVE'); // TestUserStatus::ACTIVE

Bonnes pratiques

1. Toujours typer explicitement les TypedCollection

// ✅ BON - Type explicite
public readonly TypedCollection $tags = new TypedCollection('string');

2. Utiliser EmptyRecord plutôt que null

// ✅ BON
public readonly Recordable $filters = new EmptyRecord();

3. Préférer les BackedEnum

// ✅ Recommandé
enum UserRole: string
{
    case ADMIN = 'admin';
}

// ⚠️ Acceptable mais moins pratique
enum UserStatus
{
    case ACTIVE;
}

4. Ne pas mettre de logique métier dans un Record

final class UserRecord extends AbstractRecord
{
    // ✅ BON - Que des données
    public function __construct(
        public readonly string $name,
        public readonly string $email,
    ) {}
}

5. Convertir avant la construction

// Conversion avant le Record
$tags = new TypedCollection('string');
foreach ($user->tags as $tag) {
    $tags->add($tag);
}

return new UserRecord(
    name: $user->name,
    tags: $tags,  // Déjà en TypedCollection
    createdAt: $user->created_at->toISOString(),  // Déjà en string
);

6. Valeur par défaut pour les collections

// ✅ BON
public readonly TypedCollection $tags = new TypedCollection('string');

// ✅ BON - Collection de Records
public readonly TypedCollection $items = new TypedCollection(ItemRecord::class);

7. Un Record = un élément, TypedCollection = plusieurs

public readonly UserRecord $user;           // Un utilisateur
public readonly TypedCollection $users;     // Plusieurs utilisateurs

8. Utiliser every() et some() pour les validations

// Vérifier que tous les produits sont en stock
if ($products->every(fn($p) => $p->stock > 0)) {
    // Tous disponibles
}

// Vérifier qu'au moins un produit est en promotion
if ($products->some(fn($p) => $p->isOnSale)) {
    // Appliquer réduction
}

Exemples complets

Record simple

use AndyDefer\Records\AbstractRecord;

final class UserCredentialsRecord extends AbstractRecord
{
    public function __construct(
        public readonly string $email,
        public readonly string $password,
        public readonly bool $rememberMe,
    ) {}
}

// Utilisation
$credentials = new UserCredentialsRecord(
    email: 'john@example.com',
    password: 'secret',
    rememberMe: true,
);

// Insertion
DB::table('login_attempts')->insert($credentials->toArray());

Record avec Enum et TypedCollection

use AndyDefer\Records\AbstractRecord;
use AndyDefer\Records\Collections\TypedCollection;
use App\Enums\UserRole;

final class UserListFilterRecord extends AbstractRecord
{
    public function __construct(
        public readonly ?UserRole $role = null,
        public readonly ?bool $isActive = null,
        public readonly ?string $search = null,
        public readonly TypedCollection $excludedIds = new TypedCollection('int'),
        public readonly ?DashboardFilterRecord $dashboardFilters = null,
    ) {}
}

// Utilisation
$filters = new UserListFilterRecord(
    role: UserRole::ADMIN,
    isActive: true,
    excludedIds: (new TypedCollection('int'))->add(1, 2, 3),
);

Service qui utilise un Record

final class UserService
{
    public function updateUserField(UserCredentialsRecord $credentials): UserUpdateResultRecord
    {
        // On sait exactement ce qu'on reçoit
        $user = User::where('email', $credentials->email)->first();
        
        // Traitement...
        
        return new UserUpdateResultRecord(
            success: true,
            userId: $user->id,
        );
    }
}

Repository avec Record

final class UserRepository
{
    public function create(UserRecord $record): User
    {
        $id = DB::table('users')->insertGetId($record->toArray());
        return User::find($id);
    }
    
    public function update(int $id, UserRecord $record): User
    {
        DB::table('users')->where('id', $id)->update($record->toDatabase());
        return User::find($id);
    }
}

Appel API externe avec Record

final class PaymentGatewayService
{
    public function createPayment(PaymentRequestRecord $request): PaymentResponseRecord
    {
        $response = Http::post(
            'https://api.payment.com/v1/payments',
            $request->toJson()
        );
        
        return new PaymentResponseRecord(
            transactionId: $response->json('transaction_id'),
            status: $response->json('status'),
        );
    }
}

Record avec d'autres Records

final class DashboardContextRecord extends AbstractRecord
{
    public function __construct(
        public readonly UserContextRecord $user,
        public readonly DashboardFilterRecord $filters,
        public readonly string $timezone,
    ) {}
}

// Utilisation
$context = new DashboardContextRecord(
    user: new UserContextRecord(id: 1, name: 'John'),
    filters: new DashboardFilterRecord(dateRange: 'last-30-days'),
    timezone: 'UTC',
);

Manipulation de TypedCollection dans un Service

final class OrderService
{
    public function calculateTotal(OrderRecord $order): float
    {
        return $order->items->sum(fn($item) => $item->price * $item->quantity);
    }
    
    public function getExpensiveItems(OrderRecord $order, float $threshold): TypedCollection
    {
        return $order->items->filter(fn($item) => $item->price > $threshold);
    }
    
    public function getProductNames(OrderRecord $order): TypedCollection
    {
        return $order->items->map(fn($item) => $item->productName);
    }
    
    public function validateOrder(OrderRecord $order): bool
    {
        return $order->items->every(fn($item) => $item->quantity > 0)
            && $order->items->some(fn($item) => $item->price > 0);
    }
}

Utilisation avancée des StringTypedCollection

final class ContentService
{
    public function processContent(StringTypedCollection $strings): array
    {
        return $strings
            ->trim()
            ->filterEmpty()
            ->toLowercase()
            ->uniqueCaseInsensitive()
            ->slugify()
            ->wrap('**')
            ->join("\n");
    }
    
    public function extractEmails(StringTypedCollection $content): StringTypedCollection
    {
        return $content->matchingRegex('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/');
    }
}

// Utilisation
$content = new StringTypedCollection();
$content->add('  Hello World!  ', '', '  PHP 8  ', 'Contact: john@example.com', 'HELLO WORLD');

$processed = $contentService->processContent($content);
// Retourne: "**hello-world**\n**php-8**\n**contact-john-example-com**"

$emails = $contentService->extractEmails($content);
// Retourne: ['john@example.com']

API Reference

AbstractRecord

Méthode Retour Description
toArray() array<string, mixed> Convertit en tableau (conserve null)
toDatabase() array<string, mixed> Convertit en tableau (exclut null)
toJson() string Convertit en JSON

TypedCollection

Méthode Retour Description
add(...$items) self Ajoute des éléments
toArray() array Tous les éléments
count() int Nombre d'éléments
isEmpty() bool Collection vide ?
isNotEmpty() bool Collection non vide ?
getAllowedTypes() array<string> Types autorisés
firstItem() `mixed null`
first(int $limit) self N premiers éléments
lastItem() `mixed null`
last(int $limit) self N derniers éléments
every(Closure) bool Tous satisfont ?
some(Closure) bool Un satisfait ?
map(Closure) self Transforme chaque élément
filter(Closure) self Filtre les éléments
reject(Closure) self Rejette les éléments
each(Closure) self Exécute une action
sort(int) self Trie les éléments
sortBy(Closure|string, bool) self Trie par clé/fonction
reverse() self Inverse l'ordre
shuffle() self Mélange aléatoirement
sum(?Closure) int|float Somme
avg(?Closure) ?float Moyenne
max(?Closure) mixed Valeur max
min(?Closure) mixed Valeur min
ofType(string) self Filtrer par type
exceptType(string) self Exclure un type
records() self Filtrer les Records
scalars() self Filtrer les scalaires
ofRecord(string) self Filtrer par classe Record
anyRecord() self Tous les Records
getTypes() self Types distincts
where(string, mixed) self Filtrer par propriété
whereNotNull(string) self Propriété non nulle
whereNull(string) self Propriété nulle
contains(mixed) bool Élément existe ?
containsType(string) bool Type présent ?
isOnlyType(string) bool Tous du même type ?
take(int) self N premiers
skip(int) self Ignorer N premiers
slice(int, ?int) self Extraire une plage
nth(int, int) self Un élément sur N
values() self Réindexer
unique(?Closure) self Supprimer doublons
merge(self) self Fusionner
intersect(self) self Éléments communs
diff(self) self Éléments uniques
flatMap(Closure) self Aplatir
filterNull() self Supprimer null
random(int) self Éléments aléatoires
isHomogeneous() bool Tous du même type ?
isHeterogeneous() bool Types différents ?
assertAllOfType(string) self Vérifie le type
assertNotEmpty() self Vérifie non vide
assertContainsType(string) self Vérifie présence type
assertAllImplement(string) self Vérifie interface
assertScalar() self Vérifie scalaire
assertRecords() self Vérifie Record
validate(Closure) self Validation

StringTypedCollection

Méthode Retour Description
toLowercase() self Convertit en minuscules
toUppercase() self Convertit en majuscules
containsSubstring(string) self Filtre par sous-chaîne
startsWith(string) self Filtre par préfixe
endsWith(string) self Filtre par suffixe
filterEmpty() self Supprime les chaînes vides
trim(string) self Supprime les espaces
truncate(int, string) self Limite la longueur
matchingRegex(string) self Filtre par regex
join(string) string Joint toutes les chaînes
lengths() TypedCollection<int> Longueurs des chaînes
pad(int, string, int) self Padde les chaînes
replace(string|array, string|array) self Remplace des valeurs
firstCharacter() self Premier caractère
lastCharacter() self Dernier caractère
substring(int, ?int) self Extrait une sous-chaîne
countMatchingRegex(string) int Compte les regex
hasMatchingRegex(string) bool Vérifie si match
uniqueCaseInsensitive() self Valeurs uniques (insensible)
sortCaseInsensitive(bool) self Tri insensible à la casse
removeWhitespace() self Supprime les espaces
slugify() self Convertit en slug URL
wrap(string, ?string) self Encadre les chaînes
removePrefix(string) self Supprime un préfixe
removeSuffix(string) self Supprime un suffixe

IntTypedCollection

Méthode Retour Description
even() self Nombres pairs
odd() self Nombres impairs
median() float Médiane
zero() self Zéros
nonNegative() self Non négatifs

FloatTypedCollection

Méthode Retour Description
round(int) self Arrondit à une précision
ceil() self Entier supérieur
floor() self Entier inférieur
format(int) self Arrondit (alias)

BoolTypedCollection

Méthode Retour Description
trueOnly() self Uniquement true
falseOnly() self Uniquement false
countTrue() int Nombre de true
countFalse() int Nombre de false
allTrue() bool Tous true ?
allFalse() bool Tous false ?
anyTrue() bool Au moins un true ?
anyFalse() bool Au moins un false ?

NumberTypedCollection

Méthode Retour Description
positive() self Nombres positifs (> 0)
negative() self Nombres négatifs (< 0)
between(int|float, int|float) self Intervalle
average() float Moyenne
zero() self Zéros (0 ou 0.0)
nonNegative() self Non négatifs (>= 0)
areAllIntegers() bool Tous entiers ?
hasAnyFloat() bool Au moins un float ?
toFloats() FloatTypedCollection Convertit en floats
toIntegers() IntTypedCollection Convertit en ints
separateTypes() array Sépare ints et floats

AbstractNumberTypedCollection

Méthode Retour Description
positive() self Nombres positifs (> 0)
negative() self Nombres négatifs (< 0)
between(int|float, int|float) self Intervalle
average() float Moyenne
range(start, end, step) static Génère une séquence

Licence

MIT © Andy Defer