Factories Laravel : Créer des données de test réalistes avec les modèles de Factory Laravel
Lors du développement d’applications avec Laravel, l’un des goulots d’étranglement les plus courants dans le flux de travail de test est la génération de données significatives et réalistes. Les factories Laravel sont des classes qui définissent un modèle pour créer des instances de modèles Eloquent, en utilisant la bibliothèque PHP Faker pour produire des valeurs d’attributs aléatoires mais structurellement valides — permettant aux développeurs d’alimenter les bases de données et d’écrire des tests isolés sans construire manuellement des fixtures de données.
Contrairement aux fichiers de seed SQL statiques ou aux tableaux codés en dur, les factories sont composables, avec état et conscientes des relations. Elles s’intègrent directement avec les suites de tests PHPUnit et Pest, prennent en charge l’évaluation différée des attributs et s’adaptent d’une seule instance de modèle à des milliers d’enregistrements en une seule chaîne de méthodes. Si vous exécutez Laravel sur un environnement VPS Hosting, les factories deviennent particulièrement précieuses lors des exécutions de pipelines CI/CD, des réinitialisations d’environnements de staging et des scénarios de tests de charge où la génération de données répétable et contrôlée est indispensable.
Que sont les factories Laravel et pourquoi sont-elles importantes
Les factories Laravel ont été fondamentalement repensées dans Laravel 8. L’ancienne approche basée sur les closures `$factory->define()` a été remplacée par des classes PHP dédiées qui étendent `IlluminateDatabaseEloquentFactoriesFactory`. Ce changement architectural a introduit la sécurité des types, l’autocomplétion IDE et une séparation plus claire entre la logique des factories et les définitions des modèles.
Chaque classe de factory implémente une méthode `definition()` qui retourne un tableau associatif d’attributs de modèle. La factory résout automatiquement une instance `FakerGenerator`, accessible via `$this->faker`, qui prend en charge plus de 200 fournisseurs de données sensibles aux paramètres régionaux — de `name()` et `safeEmail()` à `iban()`, `latitude()`, `uuid()` et `creditCardNumber()`.
Capacités clés du système de factory moderne :
- Chaînage de méthodes fluide pour la configuration du nombre, de l’état et des relations
- Résolution différée des attributs — les closures dans `definition()` sont évaluées fraîchement pour chaque instance
- États de factory pour modéliser des variations spécifiques au domaine (par exemple, comptes suspendus, utilisateurs vérifiés)
- Factories de relations qui créent récursivement des modèles parents si nécessaire
- Séquences pour parcourir des ensembles d’attributs prédéfinis
- `make()` vs `create()` pour les instances en mémoire vs persistées
Prérequis
Avant d’implémenter des factories, assurez-vous que votre environnement répond aux exigences suivantes :
- Laravel 9 ou ultérieur (Laravel 8 est compatible mais manque de certaines fonctionnalités de séquence plus récentes)
- PHP 8.0 ou supérieur
- Une connexion de base de données configurée dans `.env` (MySQL, PostgreSQL ou SQLite pour les tests en mémoire)
- Le package `laravel/framework`, qui est livré avec `fakerphp/faker` comme dépendance
- Des modèles Eloquent avec les fichiers de migration correspondants
Pour les équipes exécutant Laravel sur une infrastructure gérée, VPS avec cPanel fournit un environnement pratique pour gérer à la fois la pile applicative et les services de base de données depuis une interface unifiée.
Étape 1 : Générer une classe de factory
Utilisez l’interface CLI Artisan pour créer un fichier de factory :
“`bash
php artisan make:factory UserFactory
“`
Cela crée `database/factories/UserFactory.php`. Si vous souhaitez associer automatiquement la factory à un modèle, passez le flag `–model` :
“`bash
php artisan make:factory UserFactory –model=User
“`
Laravel résout la liaison factory-modèle via une convention de nommage : `UserFactory` correspond à `AppModelsUser`. Vous pouvez remplacer cela en définissant explicitement la propriété `protected $model`, ce qui est essentiel lorsque vos modèles se trouvent en dehors de l’espace de noms `AppModels` par défaut.
Étape 2 : Définir le modèle de factory
Ouvrez `database/factories/UserFactory.php` et définissez la méthode `definition()` :
“`php
<?php
namespace DatabaseFactories;
use AppModelsUser;
use IlluminateDatabaseEloquentFactoriesFactory;
use IlluminateSupportStr;
use IlluminateSupportFacadesHash;
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => Hash::make('password'),
'remember_token' => Str::random(10),
];
}
}
“`
Notes au niveau des attributs :
- `$this->faker->unique()->safeEmail()` — le modificateur `unique()` maintient un registre d’unicité par requête. Si vous épuisez les valeurs uniques disponibles (rare mais possible avec de très grands ensembles de données), Faker lève une `OverflowException`. Réinitialisez-la avec `$this->faker->unique(true)` pour vider le cache.
- `Hash::make('password')` est préféré à `bcrypt()` directement car il respecte le pilote de hachage configuré de l’application (bcrypt, argon2i, argon2id).
- `email_verified_at => now()` marque l’utilisateur comme déjà vérifié. Omettez ce champ ou définissez-le sur `null` pour simuler un compte non vérifié — une variation d’état courante.
Étape 3 : Créer des instances de modèle
3.1 Persister un seul enregistrement
“`php
$user = AppModelsUser::factory()->create();
“`
Cela exécute une instruction `INSERT` et retourne un modèle Eloquent `User` hydraté. L’instance retournée reflète l’état réel de la base de données, y compris les valeurs par défaut au niveau de la base de données ou les déclencheurs.
3.2 Persister plusieurs enregistrements
“`php
$users = AppModelsUser::factory()->count(10)->create();
“`
Retourne une `IlluminateDatabaseEloquentCollection` de 10 instances `User`. Chaque enregistrement reçoit des valeurs Faker générées indépendamment — ce ne sont pas des copies d’une seule instance.
3.3 Instance en mémoire sans persistance
“`php
$user = AppModelsUser::factory()->make();
“`
La méthode `make()` instancie le modèle et remplit ses attributs sans toucher à la base de données. C’est idéal pour les tests unitaires qui vérifient le comportement du modèle, le casting des attributs ou la logique des accesseurs/mutateurs de manière isolée — gardant les tests rapides et indépendants de la base de données.
3.4 Remplacer des attributs spécifiques
`create()` et `make()` acceptent tous deux un tableau de remplacements d’attributs :
“`php
$user = AppModelsUser::factory()->create([
'email' => 'specific@example.com',
'name' => 'Jane Doe',
]);
“`
Les remplacements ont priorité sur les valeurs `definition()`. C’est le bon modèle lorsqu’un test dépend d’une valeur d’attribut spécifique et connue plutôt que d’une valeur aléatoire.
Étape 4 : États de factory
Les états sont des modifications nommées de la définition de factory de base. Ils vous permettent de modéliser des conditions de domaine distinctes sans dupliquer l’intégralité de la factory.
4.1 Définir des états
“`php
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'is_admin' => true,
'role' => 'administrator',
]);
}
public function suspended(): static
{
return $this->state(fn (array $attributes) => [
'suspended_at' => now(),
'is_active' => false,
]);
}
“`
4.2 Appliquer des états
“`php
// Single state
$adminUser = AppModelsUser::factory()->admin()->create();
// Stacked states — fully composable
$suspendedAdmin = AppModelsUser::factory()->admin()->suspended()->create();
“`
Les états sont évalués dans l’ordre dans lequel ils sont chaînés. Les états ultérieurs écrasent les clés conflictuelles des états précédents, vous donnant une résolution d’attributs prévisible et en couches.
Étape 5 : Séquences pour les valeurs d’attributs cycliques
Lorsque vous devez alterner entre un ensemble défini de valeurs plutôt que des valeurs aléatoires, utilisez `Sequence` :
“`php
use IlluminateDatabaseEloquentFactoriesSequence;
$users = AppModelsUser::factory()
->count(6)
->state(new Sequence(
['role' => 'editor'],
['role' => 'viewer'],
['role' => 'moderator'],
))
->create();
“`
Cela parcourt le tableau de séquence en attribuant les rôles dans l’ordre. Avec 6 utilisateurs, chaque rôle est attribué deux fois. Les séquences sont indispensables pour tester la pagination, le contrôle d’accès basé sur les rôles et la logique de rendu UI qui dépend de distributions de données variées mais contrôlées.
Étape 6 : Factories de relations
6.1 Définir une relation Belongs-To
Dans `PostFactory.php`, référencez directement la factory parente comme valeur d’attribut :
“`php
<?php
namespace DatabaseFactories;
use AppModelsPost;
use AppModelsUser;
use IlluminateDatabaseEloquentFactoriesFactory;
class PostFactory extends Factory
{
protected $model = Post::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => $this->faker->sentence(),
'body' => $this->faker->paragraphs(3, true),
'slug' => $this->faker->unique()->slug(),
];
}
}
“`
Lorsque `user_id` est défini sur `User::factory()`, Laravel diffère son évaluation. Si vous appelez `Post::factory()->create()` sans fournir de `user_id`, un nouveau `User` est automatiquement créé et sa clé primaire est utilisée. Si vous fournissez un utilisateur existant, la factory imbriquée est entièrement ignorée.
6.2 Attacher à un parent existant
“`php
$user = AppModelsUser::factory()->create();
$posts = AppModelsPost::factory()->count(5)->for($user)->create();
“`
La méthode `for()` définit la clé étrangère `user_id` sur la clé primaire du modèle fourni, évitant la création inutile d’utilisateurs. C’est le bon modèle lorsque votre test a déjà un utilisateur spécifique dans sa portée.
6.3 Relations Has-Many
“`php
$userWithPosts = AppModelsUser::factory()
->has(AppModelsPost::factory()->count(3), 'posts')
->create();
“`
Ou en utilisant le raccourci magique `hasPosts()` (résolu via le nom de la méthode de relation sur le modèle) :
“`php
$userWithPosts = AppModelsUser::factory()->hasPosts(3)->create();
“`
Cela crée un utilisateur et trois publications associées en une seule opération atomique — avec toutes les clés étrangères résolues correctement.
6.4 Relations Many-to-Many
“`php
$user = AppModelsUser::factory()
->hasAttached(
AppModelsRole::factory()->count(2),
['assigned_at' => now()]
)
->create();
“`
La méthode `hasAttached()` gère l’insertion dans la table pivot, y compris tous les attributs pivot supplémentaires que vous devez renseigner.
Étape 7 : Utiliser les factories dans les tests
7.1 Test de fonctionnalité avec assertions de base de données
“`php
use IlluminateFoundationTestingRefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_be_created_with_factory(): void
{
$user = AppModelsUser::factory()->create();
$this->assertDatabaseHas('users', [
'email' => $user->email,
]);
}
public function test_unverified_user_cannot_access_dashboard(): void
{
$user = AppModelsUser::factory()->unverified()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertRedirect('/email/verify');
}
}
“`
Détail critique : Utilisez toujours le trait `RefreshDatabase` ou `DatabaseTransactions` dans les classes de test qui interagissent avec la base de données. `RefreshDatabase` exécute les migrations fraîchement avant la suite de tests et enveloppe chaque test dans une transaction qui est annulée ensuite, gardant les tests isolés et idempotents.
7.2 Test unitaire avec `make()`
“`php
public function test_user_full_name_accessor(): void
{
$user = AppModelsUser::factory()->make([
'name' => 'Alice Wonderland',
]);
$this->assertEquals('Alice Wonderland', $user->name);
}
“`
Aucune interaction avec la base de données ne se produit. Le test s’exécute en microsecondes et convient aux pipelines CI à haute fréquence.
Étape 8 : Seeders de base de données avec les factories
8.1 Créer un seeder
“`bash
php artisan make:seeder UserSeeder
“`
8.2 Implémenter le seeder
“`php
<?php
namespace DatabaseSeeders;
use AppModelsUser;
use IlluminateDatabaseSeeder;
class UserSeeder extends Seeder
{
public function run(): void
{
User::factory()
->count(50)
->create();
}
}
“`
8.3 Seeder composite avec relations
“`php
<?php
namespace DatabaseSeeders;
use AppModelsUser;
use AppModelsPost;
use IlluminateDatabaseSeeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()
->count(20)
->has(Post::factory()->count(5), 'posts')
->create();
// Create 5 admin users with no posts
User::factory()->count(5)->admin()->create();
}
}
“`
8.4 Exécuter le seeder
“`bash
Run a specific seeder
php artisan db:seed –class=UserSeeder
Run all seeders defined in DatabaseSeeder
php artisan db:seed
Migrate fresh and seed in one command (common in staging resets)
php artisan migrate:fresh –seed
“`
La commande `migrate:fresh –seed` est le flux de travail standard pour réinitialiser une base de données de staging ou de développement à un état connu et peuplé. Sur les Serveurs Dédiés, ce modèle est fréquemment utilisé avant les cycles d’assurance qualité pour garantir un environnement propre et reproductible.
Modèles avancés et cas limites
Attributs différés et valeurs dépendantes
Les valeurs Faker dans `definition()` sont réévaluées pour chaque appel de factory. Cependant, si vous avez besoin qu’un attribut dépende d’un autre, utilisez une closure :
“`php
public function definition(): array
{
$firstName = $this->faker->firstName();
$lastName = $this->faker->lastName();
return [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => strtolower("{$firstName}.{$lastName}@example.com"),
'username' => strtolower("{$firstName}{$lastName}") . $this->faker->numerify('###'),
];
}
“`
Cela garantit que `email` et `username` sont dérivés des mêmes valeurs de nom, produisant des enregistrements cohérents en interne.
Éviter le piège de dépassement `unique()`
Lors de la génération de grands ensembles de données (10 000+ enregistrements en un seul appel de factory), `$this->faker->unique()->safeEmail()` peut épuiser le pool d’unicité de Faker et lever une `OverflowException`. Atténuez cela en ajoutant un UUID ou un horodatage à la valeur générée :
“`php
'email' => $this->faker->safeEmail() . '.' . $this->faker->uuid() . '@test.com',
“`
Cela garantit l’unicité à grande échelle sans dépendre du registre d’unicité interne de Faker.
Callbacks de factory : `afterMaking` et `afterCreating`
Utilisez des callbacks pour effectuer une logique post-création qui ne peut pas être exprimée comme un simple attribut :
“`php
public function configure(): static
{
return $this->afterCreating(function (User $user) {
$user->profile()->create([
'bio' => $this->faker->paragraph(),
'avatar' => $this->faker->imageUrl(200, 200, 'people'),
]);
});
}
“`
La méthode `configure()` est appelée une fois lorsque la factory est instanciée. `afterCreating` s’exécute après que le modèle est persisté, ce qui le rend adapté à la création de modèles liés qui nécessitent la clé primaire du parent.
Tester les fonctionnalités d’e-mail
Lorsque les factories génèrent des utilisateurs avec des adresses e-mail, les tests d’intégration qui vérifient l’envoi d’e-mails bénéficient d’un environnement Email Hosting dédié configuré avec un serveur SMTP sandbox, empêchant la livraison accidentelle d’e-mails de test à de vraies adresses.
`create()` vs `make()` vs `makeMany()` vs `createMany()` — Comparaison
| Méthode | Persiste en BDD | Retourne | Meilleur cas d’utilisation |
|---|---|---|---|
| — | — | — | — |
| `create()` | Oui | Instance de modèle unique | Tests de fonctionnalité, seeders |
| `create(['key' => 'val'])` | Oui | Instance de modèle unique | Tests nécessitant des valeurs connues spécifiques |
| `count(n)->create()` | Oui | Collection de n modèles | Alimentation en masse, tests de pagination |
| `make()` | Non | Instance de modèle unique | Tests unitaires, test des accesseurs/mutateurs |
| `make(['key' => 'val'])` | Non | Instance de modèle unique | Tests unitaires rapides avec attributs contrôlés |
| `count(n)->make()` | Non | Collection de n modèles | Tests de collection en mémoire |
| `createMany([…])` | Oui | Collection | Création par lots avec des ensembles d’attributs distincts |
| `makeMany([…])` | Non | Collection | Instances en mémoire par lots |
États de factory vs remplacements d’attributs — Quand utiliser chacun
| Scénario | Approche recommandée |
|---|---|
| — | — |
| Condition de domaine réutilisable (par exemple, « utilisateur admin ») | Méthode d’état nommée |
| Valeur ponctuelle spécifique au test | Remplacement d’attribut dans `create()` |
| Parcourir un ensemble prédéfini | `Sequence` |
| Effets secondaires post-création | Callback `afterCreating` |
| Valeurs d’attributs dépendantes | Attributs basés sur des closures dans `definition()` |
| Population des relations | `has()`, `for()`, `hasAttached()` |
Liste de contrôle pratique pour la prise de décision
Avant d’écrire une factory ou un test qui en utilise une, parcourez ces points de contrôle :
- Le test dépend-il de la base de données ? Si non, utilisez `make()` et évitez la surcharge `RefreshDatabase`.
- Le test nécessite-t-il une valeur d’attribut spécifique ? Passez-la comme remplacement à `create()` — ne la codez pas en dur dans la factory `definition()`.
- Testez-vous un comportement basé sur les rôles ? Définissez des états nommés plutôt que de disperser `create(['is_admin' => true])` dans plusieurs fichiers de test.
- Alimentez-vous un environnement de staging ? Utilisez `migrate:fresh –seed` et assurez-vous que votre `DatabaseSeeder` compose tous les sous-seeders dans le bon ordre de dépendance (parents avant enfants).
- Générez-vous plus de 5 000 enregistrements ? Évitez `unique()` sur les champs à haute cardinalité ; utilisez plutôt des valeurs suffixées par UUID.
- Vos modèles ont-ils des callbacks `afterCreating` qui accèdent à des services externes ? Simulez ces services dans la configuration de votre test ou utilisez `make()` pour contourner entièrement le callback.
- Exécutez-vous des tests en parallèle ? Utilisez `DatabaseTransactions` plutôt que `RefreshDatabase` pour éviter les conflits de migration entre les workers parallèles, ou configurez des connexions de base de données séparées par worker.
Pour les équipes gérant plusieurs applications Laravel dans différents environnements, les Panneaux de contrôle VPS fournissent la visibilité d’infrastructure nécessaire pour surveiller les performances de la base de données lors des grandes opérations d’alimentation et des exécutions de tests.
FAQ
Quelle est la différence entre `create()` et `make()` dans les factories Laravel ?
`create()` persiste le modèle dans la base de données et retourne une instance Eloquent hydratée. `make()` construit le modèle en mémoire sans aucune interaction avec la base de données. Utilisez `make()` pour les tests unitaires purs afin de les garder rapides et isolés ; utilisez `create()` lorsque le test doit vérifier l’état de la base de données.
Les factories Laravel peuvent-elles gérer les relations polymorphiques ?
Oui. Définissez une relation `morphTo` en définissant les colonnes morph `*_type` et `*_id` directement dans la factory `definition()`, ou utilisez `afterCreating` pour attacher des relations polymorphiques après que le modèle parent est persisté. Il n’y a pas de raccourci `hasMorphedByMany()` intégré, donc la définition explicite des attributs est l’approche la plus fiable.
Comment empêcher l’envoi des e-mails générés par les factories pendant les tests ?
Définissez `MAIL_MAILER=array` ou `MAIL_MAILER=log` dans votre fichier `.env.testing`. Cela achemine tout le courrier via le pilote array ou log de Laravel, capturant les messages en mémoire ou les écrivant dans le fichier journal sans les envoyer à un serveur SMTP. Vous pouvez ensuite effectuer des assertions sur `Mail::assertSent()` dans vos tests.
Pourquoi `faker->unique()->safeEmail()` lève-t-il une `OverflowException` avec de grands ensembles de données ?
Le modificateur `unique()` de Faker maintient un registre en mémoire des valeurs précédemment générées. Lorsque le pool de valeurs uniques structurellement valides est épuisé — ce qui peut arriver avec des dizaines de milliers d’enregistrements — il lève `OverflowException`. La solution consiste à ajouter un UUID ou une chaîne aléatoire à la valeur d’e-mail de base, garantissant l’unicité sans dépendre du registre de Faker.
Les factories doivent-elles être utilisées dans les seeders de production ?
Les factories sont conçues pour les environnements de développement et de test. Pour l’alimentation en production (par exemple, remplissage des tables de référence, rôles par défaut ou enregistrements de configuration), utilisez des classes de seeder dédiées avec des valeurs codées en dur et déterministes. Les factories qui dépendent de Faker ne doivent jamais s’exécuter contre une base de données de production, car elles introduisent des données imprévisibles et non auditables.
