Aller au contenu principal

Module 13 – Base de données avec Eloquent

Niveau 3 – Laravel fondamental


Objectifs

Migrations, modèles Eloquent (CRUD, attributs, casts), relations en profondeur (belongsTo, hasMany, hasOne, belongsToMany, hasManyThrough), relations polymorphiques, eager loading et N+1, seeders et factories pour les données de test.


Théorie

Migrations

Les migrations décrivent les changements de schéma (création/modification de tables) de façon versionnée.

php artisan make:migration create_articles_table

Dans le fichier généré (database/migrations/...) :

public function up(): void
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body')->nullable();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('articles'); }

Exécuter : php artisan migrate. Annuler : php artisan migrate:rollback.


Modèles Eloquent

Un modèle = une table. Convention : modèle Article → table articles, clé primaire id.

php artisan make:model Article
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
protected $fillable = ['title', 'body', 'user_id'];
protected $casts = ['published_at' => 'datetime'];
}

CRUD :

Article::all();
Article::find(1);
Article::where('user_id', 1)->get();
Article::create(['title' => 'My title', 'body' => '...', 'user_id' => 1]);
$article->update(['title' => 'New title']);
$article->delete();

Relations Eloquent – En profondeur

Une relation relie deux modèles via des clés (souvent clé étrangère). Eloquent suppose par défaut des noms de colonnes : user_id pour un belongsTo(User::class), table users, clé id. On peut surcharger ces conventions si besoin.

One-to-One : hasOne / belongsTo

Un enregistrement d’un modèle correspond à au plus un enregistrement d’un autre. Exemple : un User a un Profile.

// User has one Profile (table profiles avec user_id)
public function profile()
{
return $this->hasOne(Profile::class);
}

// Profile belongs to User
public function user()
{
return $this->belongsTo(User::class);
}

Convention : la table profiles contient user_id. Sinon : hasOne(Profile::class, 'foreign_key', 'local_key').

Utilisation :

$user->profile;           // lazy
$user->profile()->create(['bio' => '...']);

One-to-Many : hasMany / belongsTo

Un modèle « parent » a plusieurs enregistrements « enfants ». Exemple : un User a plusieurs Articles ; un Article appartient à un User.

// User has many Articles
public function articles()
{
return $this->hasMany(Article::class);
}

// Article belongs to User
public function user()
{
return $this->belongsTo(User::class);
}

Convention : table articles avec colonne user_id. Options : hasMany(Article::class, 'foreign_key', 'local_key').

Utilisation :

$user->articles;                          // Collection
$user->articles()->where('published', true)->get();
$user->articles()->create(['title' => '...', 'body' => '...']);
$article->user; // User
$article->user_id = 2; $article->save(); // ou
$article->user()->associate($user); $article->save();

Many-to-Many : belongsToMany

Plusieurs enregistrements d’un modèle sont liés à plusieurs enregistrements d’un autre, via une table pivot. Exemple : Article et Tag (un article a plusieurs tags, un tag est sur plusieurs articles).

Table pivot : nom par ordre alphabétique des deux modèles singuliers : article_tag avec article_id et tag_id.

// Migration pivot : create_article_tag_table
Schema::create('article_tag', function (Blueprint $table) {
$table->foreignId('article_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['article_id', 'tag_id']);
});
// Article has many Tags (through pivot)
public function tags()
{
return $this->belongsToMany(Tag::class);
}

// Tag has many Articles (through pivot)
public function articles()
{
return $this->belongsToMany(Article::class);
}

Si le nom de la pivot ou les clés diffèrent :

return $this->belongsToMany(Tag::class, 'article_tag', 'article_id', 'tag_id');

Utilisation :

$article->tags;                    // Collection de Tag
$article->tags()->attach($tagId);
$article->tags()->attach($tagId, ['order' => 1]); // pivot avec colonnes extra
$article->tags()->detach($tagId);
$article->tags()->sync([1, 2, 3]); // remplace la liste
$article->tags()->syncWithoutDetaching([1, 2]); // ajoute sans enlever

Colonnes supplémentaires sur la pivot : les déclarer dans la relation avec ->withPivot('order') et éventuellement ->withTimestamps().


Has Many Through (à travers un modèle intermédiaire)

Un modèle accède à des enregistrements d’un troisième modèle via un modèle intermédiaire. Exemple : Country → hasMany User → User hasMany Post ; on veut « les posts d’un pays ».

// Country has many Posts through Users
public function posts()
{
return $this->hasManyThrough(Post::class, User::class);
}

Convention : tables countries, users (avec country_id), posts (avec user_id). Sinon : hasManyThrough(Post::class, User::class, 'country_id', 'user_id').


Eager loading et problème N+1

Sans eager loading, afficher une liste d’articles avec le nom de l’auteur provoque une requête par article pour charger le userN+1 (1 pour la liste + N pour les auteurs).

Solution : charger la relation en une seule requête avec with() :

$articles = Article::with('user')->get();
foreach ($articles as $article) {
echo $article->user->name; // pas de requête supplémentaire
}

Plusieurs relations :

Article::with(['user', 'tags'])->get();

Eager load imbriqué :

Article::with('comments.user')->get();

Lazy eager load (déjà une collection en mémoire) :

$articles->load('user');

Condition sur la relation chargée :

Article::with(['comments' => fn ($q) => $q->where('approved', true)])->get();

Relations polymorphiques

Une relation polymorphique permet à un modèle d’appartenir à plusieurs types de modèles différents via une seule association. Exemple typique : des Commentaires peuvent être attachés à un Article ou à une Video, sans dupliquer la logique.

Structure en base

Au lieu de article_id ou video_id, on utilise deux colonnes sur la table des commentaires :

  • commentable_id : l’ID de l’enregistrement (article ou video).
  • commentable_type : le type (classe) du modèle, ex. App\Models\Article ou App\Models\Video.
// Migration comments
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->foreignId('user_id')->constrained();
$table->unsignedBigInteger('commentable_id');
$table->string('commentable_type');
$table->timestamps();
});

Côté modèle « enfant » : morphTo

Le modèle qui appartient à plusieurs types (ex. Comment) utilise morphTo :

// Comment.php
public function commentable()
{
return $this->morphTo();
}

Convention : colonnes commentable_id et commentable_type. Sinon : morphTo('commentable', 'type', 'id') (nom de la relation, colonne type, colonne id).

Côté modèles « parents » : morphMany / morphOne

Chaque modèle qui peut avoir des commentaires définit morphMany (ou morphOne si un seul) :

// Article.php
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}

// Video.php
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}

Utilisation :

$article->comments;                    // tous les commentaires de l'article
$article->comments()->create(['body' => '...', 'user_id' => 1]);
$comment->commentable; // Article ou Video selon commentable_type
$comment->commentable->title; // accès aux attributs du parent

Many-to-Many polymorphique : morphToMany / morphedByMany

Un modèle peut être lié à plusieurs types via une table pivot. Exemple : Tag peut être utilisé pour des Articles et des Videos.

Table pivot (ex. taggables) : tag_id, taggable_id, taggable_type.

// Tag.php : un tag peut être attaché à des articles ou des videos
public function articles()
{
return $this->morphedByMany(Article::class, 'taggable');
}
public function videos()
{
return $this->morphedByMany(Video::class, 'taggable');
}

// Article.php (et Video.php) : un article a plusieurs tags
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}

Utilisation : comme un belongsToMany classique : $article->tags, $article->tags()->attach($tag), $tag->articles, etc.


Récapitulatif des relations

RelationCôté modèleExemple d’usage
belongsToEnfant → parentArticle appartient à User
hasOneParent → enfantUser a un Profile
hasManyParent → enfantsUser a plusieurs Articles
belongsToManyLes deux (pivot)Article ↔ Tag (table article_tag)
hasManyThroughParent → petits-enfantsCountry → Posts via Users
morphToEnfant polymorphiqueComment → Article ou Video
morphMany / morphOneParentArticle a plusieurs Comments
morphToMany / morphedByManyMany-to-many polymorphiqueArticle ↔ Tag, Video ↔ Tag

Seeders et Factories

Factory : génère des données fictives pour un modèle.

php artisan make:factory ArticleFactory

Seeder : remplit la base (appel aux factories ou insert manuel).

php artisan make:seeder ArticleSeeder
php artisan db:seed

Exemples rapides

hasOne / belongsTo : User → Profile (table profiles avec user_id).
hasMany / belongsTo : User → Articles (table articles avec user_id).
belongsToMany : Articles ↔ Tags via table pivot article_tag (article_id, tag_id).
Polymorphique : Comment morphTo commentable ; Article et Video morphMany Comment (colonnes commentable_id, commentable_type).


Quiz – Module 13

Q1. À quoi sert une migration ?
Q2. Quelle table et quelle clé primaire sont utilisées par défaut pour le modèle Article ?
Q3. Quelle méthode utiliser pour éviter le problème N+1 lors du chargement des relations ?
Q4. Quelle relation utiliser : « un article appartient à un utilisateur » ?
Q5. À quoi servent les factories ?
Q6. Pour une relation many-to-many entre Article et Tag, comment s’appelle la table pivot par convention ? Comment attacher/détacher un tag à un article ?
Q7. Qu’est-ce qu’une relation polymorphique ? Donner un exemple (modèle + colonnes en base).
Q8. Quelle méthode Eloquent utilise-t-on côté modèle « enfant » (ex. Comment) pour une relation polymorphique, et quelle(s) côté « parent » (ex. Article) ?

Réponses

R1. À décrire les changements de structure de la base (création/modification de tables) de façon versionnée et reproductible.

R2. Table articles, clé primaire id.

R3. with('relation') (eager loading) : ex. Article::with('user')->get().

R4. belongsTo dans le modèle Article vers User.

R5. À générer des données fictives pour les modèles (tests, seeders, développement).

R6. Table pivot par convention : article_tag (ordre alphabétique des noms singuliers), avec article_id et tag_id. Attacher : $article->tags()->attach($tagId) ; détacher : $article->tags()->detach($tagId) ; remplacer la liste : $article->tags()->sync([1, 2, 3]).

R7. Une relation polymorphique permet à un modèle d’appartenir à plusieurs types de modèles via une seule association. Exemple : Comment peut appartenir à Article ou Video ; en base : colonnes commentable_id (l’id) et commentable_type (la classe, ex. App\Models\Article).

R8. Côté enfant (Comment) : morphTo() (ex. commentable()). Côté parent (Article) : morphMany() ou morphOne() (ex. comments() avec morphMany(Comment::class, 'commentable')).


Suite

Module 14 – Authentification & Autorisation (Auth Laravel, guards, policies, gates).