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);
← Module 03 ▶ Mini-projet 🧠 QCM Module 05 →