Models Eloquent
php artisan make:model Article -mfc # Model + Migration + Factory + Controller
// app/Models/Article.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Article extends Model
{
use HasFactory, SoftDeletes;
// Colonnes assignables en masse
protected $fillable = ['title', 'body', 'status', 'author_id', 'category_id', 'published_at'];
// OU tout sauf :
// protected $guarded = ['id'];
// Conversions automatiques
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
'tags' => 'array', // JSON ↔ array PHP
'status' => \App\Enums\ArticleStatus::class, // Enum PHP 8.1
];
// Colonnes masquées dans le JSON
protected $hidden = ['internal_notes'];
// Attributs ajoutés à la sérialisation
protected $appends = ['reading_time'];
// Accesseur (PHP 8+ style)
public function readingTime(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::get(
fn() => ceil(str_word_count($this->body) / 200) . ' min'
);
}
}
Migrations
php artisan make:migration create_articles_table
php artisan make:migration add_category_id_to_articles_table --table=articles
// database/migrations/xxxx_create_articles_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('excerpt')->nullable();
$table->longText('body');
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
$table->boolean('is_featured')->default(false);
$table->json('tags')->nullable();
$table->timestamp('published_at')->nullable();
// Clés étrangères
$table->foreignId('author_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
$table->timestamps(); // created_at + updated_at
$table->softDeletes(); // deleted_at
});
}
public function down(): void {
Schema::dropIfExists('articles');
}
};
Queries
// ── Récupérer ──────────────────────────────────────────
$all = Article::all();
$one = Article::find(1);
$orFail = Article::findOrFail(999); // 404 si absent
$first = Article::first();
$count = Article::count();
// Conditions
$published = Article::where('status', 'published')->get();
$recent = Article::where('created_at', '>=', now()->subDays(7))->get();
$multiple = Article::whereIn('id', [1, 2, 3])->get();
$notNull = Article::whereNotNull('published_at')->get();
// Chaînage
$articles = Article::where('status', 'published')
->where('is_featured', true)
->orWhere('author_id', auth()->id())
->latest() // orderBy created_at DESC
->orderBy('title') // tri supplémentaire
->limit(10)
->get();
// Pagination
$articles = Article::paginate(15); // LengthAwarePaginator
$articles = Article::simplePaginate(15); // SimplePaginator
$articles = Article::cursorPaginate(15); // CursorPaginator (performant)
// Récupérer OU créer
$tag = Tag::firstOrCreate(['name' => 'Laravel'], ['color' => '#FF2D20']);
// Mettre à jour OU créer
$setting = Setting::updateOrCreate(
['key' => 'theme'], // critères
['value' => 'dark'] // données
);
// ── Créer ──────────────────────────────────────────────
$article = Article::create(['title' => 'Mon titre', 'body' => '...', 'status' => 'draft']);
$article = new Article();
$article->title = 'Mon titre';
$article->save();
// ── Modifier ───────────────────────────────────────────
Article::where('status', 'draft')->update(['status' => 'published']);
$article->update(['title' => 'Nouveau titre']);
$article->title = 'Nouveau titre';
$article->save();
// ── Supprimer ──────────────────────────────────────────
$article->delete(); // soft delete si trait activé
Article::destroy([1, 2, 3]); // plusieurs IDs
Article::where('status', 'archived')->delete();
// Soft delete
Article::withTrashed()->get(); // inclut deleted
Article::onlyTrashed()->get(); // seulement deleted
$article->restore(); // restaurer
$article->forceDelete(); // vraie suppression
Relations
// ── hasMany / belongsTo ────────────────────────────────
class User extends Model {
// Un user a plusieurs articles
public function articles(): HasMany {
return $this->hasMany(Article::class, 'author_id');
}
}
class Article extends Model {
// Un article appartient à un user
public function author(): BelongsTo {
return $this->belongsTo(User::class, 'author_id');
}
// Un article a plusieurs commentaires
public function comments(): HasMany {
return $this->hasMany(Comment::class);
}
}
// ── belongsToMany (N:N) ────────────────────────────────
class Article extends Model {
public function tags(): BelongsToMany {
return $this->belongsToMany(Tag::class)
->withTimestamps() // pivot a timestamps
->withPivot('position'); // colonne pivot supplémentaire
}
}
// Manipuler la relation N:N
$article->tags()->attach([1, 2, 3]);
$article->tags()->detach(2);
$article->tags()->sync([1, 3, 4]); // remplace tout
$article->tags()->toggle(5); // add si absent, remove si présent
// Accéder au pivot
foreach ($article->tags as $tag) {
echo $tag->pivot->position;
echo $tag->pivot->created_at;
}
// ── Eager Loading (N+1 problem) ────────────────────────
// ❌ N+1 — 1 requête pour articles + N requêtes pour auteurs
$articles = Article::all();
foreach ($articles as $a) { echo $a->author->name; } // N requêtes !
// ✅ Eager loading — 2 requêtes au total
$articles = Article::with('author', 'tags', 'category')->paginate(15);
// Eager loading conditionnel
$articles = Article::with(['comments' => function($q) {
$q->where('approved', true)->latest()->limit(3);
}])->get();
// Lazy eager loading (après coup)
$articles = Article::all();
$articles->load('author', 'tags');
// ── hasManyThrough ─────────────────────────────────────
class Country extends Model {
// Pays → Users → Articles
public function articles(): HasManyThrough {
return $this->hasManyThrough(Article::class, User::class);
}
}
Toujours eager loader les relations accédées dans les boucles. Installe
barryvdh/laravel-debugbar en développement pour détecter les N+1 automatiquement.
Factories
// database/factories/ArticleFactory.php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class ArticleFactory extends Factory
{
public function definition(): array {
return [
'title' => $this->faker->sentence(6),
'slug' => $this->faker->unique()->slug(),
'excerpt' => $this->faker->paragraph(2),
'body' => $this->faker->paragraphs(5, true),
'status' => $this->faker->randomElement(['draft', 'published']),
'is_featured' => $this->faker->boolean(20), // 20% true
'author_id' => User::factory(), // crée un user si besoin
'published_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
];
}
// États personnalisés
public function published(): static {
return $this->state([
'status' => 'published',
'published_at' => now(),
]);
}
public function draft(): static {
return $this->state(['status' => 'draft', 'published_at' => null]);
}
public function featured(): static {
return $this->state(['is_featured' => true]);
}
}
// Utilisation
Article::factory()->create(); // 1 article
Article::factory()->count(50)->create(); // 50 articles
Article::factory()->published()->count(10)->create(); // 10 publiés
Article::factory()->featured()->published()->create(); // 1 mis en avant publié
// Avec relations
Article::factory()
->for(User::factory()->state(['role' => 'author']), 'author')
->has(Comment::factory()->count(3))
->create();
Seeders
// database/seeders/DatabaseSeeder.php
namespace Database\Seeders;
use App\Models\Article;
use App\Models\Category;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void {
// Créer des utilisateurs
User::factory()->create([
'name' => 'Admin',
'email' => 'admin@test.com',
'role' => 'admin',
]);
User::factory()->count(10)->create();
// Créer des catégories
$categories = Category::factory()->count(5)->create();
// Créer des tags
$tags = collect(['PHP', 'Laravel', 'API', 'Docker', 'Tests'])
->map(fn($name) => Tag::factory()->create(['name' => $name]));
// Créer des articles avec relations
Article::factory()
->count(50)
->published()
->create()
->each(function(Article $article) use ($tags) {
$article->tags()->attach($tags->random(rand(1, 3)));
});
}
}
// Seeder spécifique
php artisan make:seeder ArticleSeeder
php artisan db:seed # Lancer DatabaseSeeder
php artisan db:seed --class=ArticleSeeder # Seeder spécifique
php artisan migrate:fresh --seed # Reset + seed
Scopes
class Article extends Model
{
// ── Local scopes ─────────────────────────────────────
public function scopePublished(Builder $query): void {
$query->where('status', 'published')
->whereNotNull('published_at');
}
public function scopeFeatured(Builder $query): void {
$query->where('is_featured', true);
}
public function scopeByAuthor(Builder $query, int $authorId): void {
$query->where('author_id', $authorId);
}
// Utilisation
// Article::published()->latest()->paginate(15)
// Article::published()->featured()->get()
// Article::byAuthor(1)->published()->get()
// ── Global scope (appliqué automatiquement) ───────────
protected static function booted(): void {
// SoftDeletes est un global scope intégré
// Exemple custom : toujours filtrer les articles actifs
}
}
// Global scope réutilisable
class PublishedScope implements Scope {
public function apply(Builder $builder, Model $model): void {
$builder->where('status', 'published');
}
}
// Enregistrement dans le model
protected static function booted(): void {
static::addGlobalScope(new PublishedScope);
}
// Désactiver un global scope
Article::withoutGlobalScope(PublishedScope::class)->get();
Observers & Events
php artisan make:observer ArticleObserver --model=Article
// app/Observers/ArticleObserver.php
namespace App\Observers;
use App\Models\Article;
class ArticleObserver
{
public function creating(Article $article): void {
// Avant création — générer le slug
if (empty($article->slug)) {
$article->slug = \Str::slug($article->title);
}
}
public function created(Article $article): void {
// Après création — log, notification...
\Log::info("Article créé : #{$article->id}");
}
public function updating(Article $article): void {
// Avant mise à jour
if ($article->isDirty('title')) {
$article->slug = \Str::slug($article->title);
}
}
public function deleting(Article $article): void {
// Avant suppression — nettoyer les relations
$article->comments()->delete();
$article->tags()->detach();
}
}
// Enregistrement dans AppServiceProvider::boot()
Article::observe(ArticleObserver::class);