Objectif
ModΓ©liser un blog complet avec Eloquent : Articles, CatΓ©gories, Tags (N:N), Commentaires, Utilisateurs. ImplΓ©menter les relations, factories, seeders, scopes et observers.
| Concept | Mis en pratique |
|---|---|
| Relations | hasMany, belongsTo, belongsToMany, hasManyThrough |
| N+1 fix | with('author', 'tags', 'category') systΓ©matique |
| Scopes | scopePublished, scopeFeatured, scopeByCategory |
| Factories | Γtats published/draft/featured, relations via for() |
| Observer | Auto-gΓ©nΓ©ration du slug, cascade delete commentaires |
| Soft deletes | Restauration d'articles supprimΓ©s |
SchΓ©ma de base de donnΓ©es
users articles categories
βββββββββ βββββββββββββ ββββββββββββββ
id id id
name title name
email slug slug
password excerpt description
role body created_at
status
is_featured tags
published_at βββββββββ
author_id β users id
category_id β categories name
created_at color
deleted_at (soft)
article_tag (pivot) comments
βββββββββββββββββββ βββββββββββββββββ
article_id id
tag_id body
position article_id β articles
user_id β users
approved
created_at
Consignes
- CrΓ©er les migrations pour les 5 tables (users dΓ©jΓ prΓ©sent)
- CrΓ©er les models avec les relations, casts, fillable, scopes
- Γcrire
ArticleFactoryavec les Γ©tatspublished(),draft(),featured() - Γcrire
DatabaseSeeder: 1 admin, 10 users, 5 catΓ©gories, 10 tags, 50 articles publiΓ©s avec tags alΓ©atoires et 3-5 commentaires chacun - Γcrire
ArticleObserver: gΓ©nΓ©ration du slug Γ la crΓ©ation, mise Γ jour si le titre change - Dans le controller, toujours eager-loader avec
with()
Solution commentΓ©e
// app/Models/Article.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
class Article extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['title', 'slug', 'excerpt', 'body', 'status', 'is_featured', 'published_at', 'author_id', 'category_id'];
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
];
// ββ Relations βββββββββββββββββββββββββββββββββββββββββ
public function author() { return $this->belongsTo(User::class, 'author_id'); }
public function category() { return $this->belongsTo(Category::class); }
public function comments() { return $this->hasMany(Comment::class); }
public function tags() {
return $this->belongsToMany(Tag::class)
->withTimestamps()
->withPivot('position')
->orderByPivot('position');
}
// ββ Scopes ββββββββββββββββββββββββββββββββββββββββββββ
public function scopePublished(Builder $q): void {
$q->where('status', 'published')->whereNotNull('published_at');
}
public function scopeFeatured(Builder $q): void {
$q->where('is_featured', true);
}
public function scopeByCategory(Builder $q, int $categoryId): void {
$q->where('category_id', $categoryId);
}
// ββ Helpers βββββββββββββββββββββββββββββββββββββββββββ
public function isPublished(): bool { return $this->status === 'published'; }
}
// app/Observers/ArticleObserver.php
class ArticleObserver
{
public function creating(Article $article): void {
$article->slug = \Str::slug($article->title);
// S'assurer de l'unicitΓ© du slug
$count = Article::withTrashed()->where('slug', 'like', $article->slug . '%')->count();
if ($count > 0) $article->slug .= '-' . ($count + 1);
}
public function updating(Article $article): void {
if ($article->isDirty('title')) {
$article->slug = \Str::slug($article->title);
}
}
public function deleting(Article $article): void {
$article->comments()->delete();
$article->tags()->detach();
}
}
// Enregistrement dans AppServiceProvider::boot()
// Article::observe(ArticleObserver::class);
// database/factories/ArticleFactory.php
class ArticleFactory extends Factory
{
public function definition(): array {
return [
'title' => $this->faker->sentence(6),
'excerpt' => $this->faker->paragraph(2),
'body' => $this->faker->paragraphs(5, true),
'status' => 'draft',
'is_featured' => false,
'published_at' => null,
'author_id' => User::factory(),
'category_id' => Category::factory(),
];
}
public function published(): static {
return $this->state([
'status' => 'published',
'published_at' => $this->faker->dateTimeBetween('-6 months', 'now'),
]);
}
public function featured(): static {
return $this->state(['is_featured' => true]);
}
}
// database/seeders/DatabaseSeeder.php
public function run(): void {
$admin = User::factory()->create([
'name' => 'Admin',
'email' => 'admin@test.com',
'role' => 'admin',
]);
User::factory()->count(10)->create();
$categories = Category::factory()->count(5)->create();
$tags = collect(['PHP', 'Laravel', 'Eloquent', 'API', 'Docker', 'Tests', 'Blade', 'Queue', 'Redis', 'MySQL'])
->map(fn($name) => Tag::create(['name' => $name, 'color' => '#' . fake()->hexColor()]));
Article::factory()
->count(50)
->published()
->recycle($categories)
->create()
->each(function (Article $article) use ($tags) {
// Attacher 1-3 tags alΓ©atoires
$article->tags()->attach(
$tags->random(rand(1, 3))->pluck('id')
);
// Ajouter 3-5 commentaires
Comment::factory()
->count(rand(3, 5))
->create(['article_id' => $article->id]);
});
}