PHPackages                             andydefer/php-records - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. andydefer/php-records

Abandoned → [andydefer/domain-structures](/?search=andydefer%2Fdomain-structures)Library[Utility &amp; Helpers](/categories/utility)

andydefer/php-records
=====================

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

1.0.0(2w ago)085↓64.7%MITPHPPHP &gt;=8.1

Since May 24Pushed 1w agoCompare

[ Source](https://github.com/andydefer/php-records)[ Packagist](https://packagist.org/packages/andydefer/php-records)[ RSS](/packages/andydefer-php-records/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (9)Versions (2)Used By (0)

PHP Records
===========

[](#php-records)

⚠️ DEPRECATED / ABANDONED
=========================

[](#️-deprecated--abandoned)

Ce package n'est plus maintenu.

**Utilisez plutôt :** [`andydefer/domain-structures`](https://github.com/andydefer/domain-structures)

Une bibliothèque PHP complète pour les structures de domaine : Value Objects, Records, Data DTO, Typed Collections, hydratation automatique et normalisation.

Migration
---------

[](#migration)

```
# Remplacer
composer remove andydefer/php-records

# Par
composer require andydefer/domain-structures
```

---

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

[![PHP Version](https://camo.githubusercontent.com/83dd395020c37276225039739320f6c8e7e99963ab21ee3d09282cb48dad2a60/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312532422d626c7565)](https://php.net)[![License](https://camo.githubusercontent.com/5caa455d8debc46fb23abbadb45a733a937f3910a73fc875c2f7820468e1bb54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e)](LICENSE)

---

Introduction
------------

[](#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

[](#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

[](#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 tableauxSolution avec RecordOn ne sait pas ce qu'ils contiennentPropriétés typées explicitesPas de validation à l'ajoutTypes garantis à la constructionDocumentation impliciteAuto-documenté par le codeRefactoring dangereuxLe compilateur guide les modifications---

Installation
------------

[](#installation)

```
composer require andydefer/php-records
```

### Prérequis

[](#prérequis)

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

---

Concept fondamental
-------------------

[](#concept-fondamental)

### Qu'est-ce qu'un Record ?

[](#quest-ce-quun-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

[](#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

[](#séparation-des-responsabilités)

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

Les Records
-----------

[](#les-records)

### Définition d'un Record

[](#définition-dun-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ègles-fondamentales)

RègleExplicationÉtendre `AbstractRecord`Tous les Records doivent hériter de la classe abstraiteNommage `{Description}Record`Ex: `UserRecord`, `PaymentResultRecord`Propriétés `public`La sérialisation automatique utilise la réflexion`readonly` recommandéImmuabilité garantiePas de logique métierNi `isValid()`, ni `save()`, ni autre méthode métier### Types autorisés dans un Record

[](#types-autorisés-dans-un-record)

TypeExempleNotes`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)

[](#types-à-éviter-ou-à-convertir-avant)

TypeAlternativePourquoi`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 8601Contient des comportements```
final class GoodRecord extends AbstractRecord
{
    public function __construct(
        public readonly TypedCollection $items,   // ✅ TypedCollection
        public readonly int $userId,              // ✅ int
        public readonly string $createdAt,        // ✅ string ISO
    ) {}
}
```

### Le Record optionnel : `EmptyRecord`

[](#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 partoutPas de conditionRisque d'erreurType-safe garanti### Sérialisation automatique

[](#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

[](#normalisation-automatique)

Type d'entréeSortie`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

[](#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
-------------------

[](#les-typedcollection)

### Définition

[](#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 ?

[](#pourquoi-remplacer-les-tableaux-)

Problème des tableauxSolution avec TypedCollectionOn ne sait pas ce qu'ils contiennentType explicite (`TypedCollection`)Pas de validation à l'ajoutValidation automatiqueModification dangereuseType-safe garantiPas de méthodes utilitairesNombreuses méthodes disponibles### Types supportés

[](#types-supportés)

TypeDescriptionExemple`'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

[](#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

[](#règle--record-vs-collection)

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

SituationType à utiliserUn 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
        public readonly TypedCollection $tags,          // TypedCollection
    ) {}
}
```

### Création d'une collection

[](#création-dune-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éthodes-de-base)

MéthodeDescriptionExemple`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

[](#transformation-et-requêtes)

MéthodeDescriptionExemple`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(Closurestring, bool): static`Trie par clé/fonction`reverse(): static`Inverse l'ordre`$collection->reverse()``shuffle(): static`Mélange aléatoirement`$collection->shuffle()`### Calculs

[](#calculs)

MéthodeDescriptionExemple`sum(?Closure): intfloat`Somme`avg(?Closure): ?float`Moyenne`$numbers->avg()``max(?Closure): mixed`Valeur max`$numbers->max()``min(?Closure): mixed`Valeur min`$numbers->min()`### Filtrage par type

[](#filtrage-par-type)

MéthodeDescriptionExemple`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

[](#recherche-et-présence)

MéthodeDescriptionExemple`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

[](#slicing-et-pagination)

MéthodeDescriptionExemple`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

[](#manipulation-avancée)

MéthodeDescriptionExemple`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

[](#validation-et-assertions)

MéthodeDescription`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
---------------------------

[](#les-collections-utilitaires)

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

### StringTypedCollection

[](#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éthodes-disponibles)

MéthodeDescriptionExemple`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`Longueurs des chaînes`$strings->lengths()``pad(int, string, int): self`Padde les chaînes`$strings->pad(10, '-')``replace(stringarray, stringarray): 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

[](#exemples-dutilisation)

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

[](#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éthodes-disponibles-1)

MéthodeDescriptionExemple`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

[](#exemples-dutilisation-1)

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

[](#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éthodes-disponibles-2)

MéthodeDescriptionExemple`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

[](#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éthodes-disponibles-3)

MéthodeDescriptionExemple`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

[](#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éthodes-disponibles-4)

MéthodeDescriptionExemple`positive(): self`Nombres positifs (&gt; 0)Hérité de `AbstractNumberTypedCollection``negative(): self`Nombres négatifs (&lt; 0)Hérité de `AbstractNumberTypedCollection``between(int|float, int|float): self`IntervalleHérité de `AbstractNumberTypedCollection``average(): float`MoyenneHérité de `AbstractNumberTypedCollection``zero(): self`Zéros (0 ou 0.0)`$numbers->zero()``nonNegative(): self`Non négatifs (&gt;= 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

[](#exemples-dutilisation-2)

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

[](#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éthodes-statiques)

MéthodeDescriptionExemple`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

[](#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
----------------------

[](#linterface-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

[](#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)

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éthodes-disponibles-5)

MéthodeDescriptionExemple`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

[](#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
----------------

[](#bonnes-pratiques)

### 1. Toujours typer explicitement les `TypedCollection`

[](#1-toujours-typer-explicitement-les-typedcollection)

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

### 2. Utiliser `EmptyRecord` plutôt que `null`

[](#2-utiliser-emptyrecord-plutôt-que-null)

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

### 3. Préférer les `BackedEnum`

[](#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

[](#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

[](#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

[](#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

[](#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

[](#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
-----------------

[](#exemples-complets)

### Record simple

[](#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

[](#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

[](#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

[](#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

[](#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

[](#record-avec-dautres-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

[](#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

[](#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
-------------

[](#api-reference)

### AbstractRecord

[](#abstractrecord)

MéthodeRetourDescription`toArray()``array`Convertit en tableau (conserve null)`toDatabase()``array`Convertit en tableau (exclut null)`toJson()``string`Convertit en JSON### TypedCollection

[](#typedcollection)

MéthodeRetourDescription`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`Types autorisés`firstItem()``mixednull``first(int $limit)``self`N premiers éléments`lastItem()``mixednull``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

[](#stringtypedcollection-1)

MéthodeRetourDescription`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`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

[](#inttypedcollection-1)

MéthodeRetourDescription`even()``self`Nombres pairs`odd()``self`Nombres impairs`median()``float`Médiane`zero()``self`Zéros`nonNegative()``self`Non négatifs### FloatTypedCollection

[](#floattypedcollection-1)

MéthodeRetourDescription`round(int)``self`Arrondit à une précision`ceil()``self`Entier supérieur`floor()``self`Entier inférieur`format(int)``self`Arrondit (alias)### BoolTypedCollection

[](#booltypedcollection-1)

MéthodeRetourDescription`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

[](#numbertypedcollection-1)

MéthodeRetourDescription`positive()``self`Nombres positifs (&gt; 0)`negative()``self`Nombres négatifs (&lt; 0)`between(int|float, int|float)``self`Intervalle`average()``float`Moyenne`zero()``self`Zéros (0 ou 0.0)`nonNegative()``self`Non négatifs (&gt;= 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

[](#abstractnumbertypedcollection-1)

MéthodeRetourDescription`positive()``self`Nombres positifs (&gt; 0)`negative()``self`Nombres négatifs (&lt; 0)`between(int|float, int|float)``self`Intervalle`average()``float`Moyenne`range(start, end, step)``static`Génère une séquence---

Licence
-------

[](#licence)

MIT © [Andy Defer](https://github.com/andydefer)

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance98

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

16d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/2170ec3fbad9eb4b002661ab4f58b1cc374eae4293b92904c6a74bc2818bd570?d=identicon)[andydefer](/maintainers/andydefer)

---

Top Contributors

[![andydefer](https://avatars.githubusercontent.com/u/124321745?v=4)](https://github.com/andydefer "andydefer (9 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm, Rector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/andydefer-php-records/health.svg)

```
[![Health](https://phpackages.com/badges/andydefer-php-records/health.svg)](https://phpackages.com/packages/andydefer-php-records)
```

###  Alternatives

[bk2k/extension-helper

Helper for TYPO3 Extension Maintenance

1257.7k21](/packages/bk2k-extension-helper)[moosend/website-tracking

By installing the Moosend PHP Tracking library you are can track page views, product views, add to cart events and successful purchases. You can later use these details to segment your user base, run automations, check how successful your latest promo has been and how many conversions your landing page has led to.

1121.6k1](/packages/moosend-website-tracking)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
