Introduction
Eloquent models are your gateway to the database. This lesson creates Task and Category models with fillable fields, relationships, query scopes, and custom methods so the rest of the app can read and write data naturally.
Key Concepts
- Eloquent model: A PHP class extending
Illuminate\Database\Eloquent\Model. $fillable: Columns allowed in mass assignment likeTask::create([...]).- Relationships:
hasMany,belongsTo,belongsToManyreturn relationship objects you can query further. - Query scopes: Reusable query constraints defined as methods prefixed with
scope. - Attribute casting: Automatically convert database columns to PHP types.
Real World Context
Well-written Eloquent models are self-documenting: reading the Task model shows you every column, every relationship, and every common query. Thin controllers plus rich models is the Laravel idiom that keeps codebases maintainable.
Deep Dive
Models are the heart of your Laravel application. They represent your data, define relationships, and contain business logic.
Creating the Category Model
Generate the model:
bashphp artisan make:model Category
Edit app/Models/Category.php:
php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class Category extends Model { /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'user_id', 'name', 'color', ]; /** * Get the user that owns the category. */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Get the tasks in this category. */ public function tasks(): HasMany { return $this->hasMany(Task::class); } }
Understanding the Model
$fillable: Lists columns that can be mass-assigned (security protection)user(): Defines the "belongs to" relationship with Usertasks(): Defines the "has many" relationship with Task
Creating the Task Model
Generate the model:
bashphp artisan make:model Task
Edit app/Models/Task.php:
php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Builder; class Task extends Model { /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'user_id', 'category_id', 'title', 'description', 'completed', 'due_date', 'priority', ]; /** * Get the attributes that should be cast. * * @return array<string, string> */ protected function casts(): array { return [ 'completed' => 'boolean', 'due_date' => 'date', ]; } /** * Get the user that owns the task. */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Get the category of the task. */ public function category(): BelongsTo { return $this->belongsTo(Category::class); } /** * Scope: Only completed tasks. */ public function scopeCompleted(Builder $query): Builder { return $query->where('completed', true); } /** * Scope: Only incomplete tasks. */ public function scopeIncomplete(Builder $query): Builder { return $query->where('completed', false); } /** * Scope: Tasks due today. */ public function scopeDueToday(Builder $query): Builder { return $query->whereDate('due_date', today()); } /** * Scope: Overdue tasks. */ public function scopeOverdue(Builder $query): Builder { return $query->where('completed', false) ->whereDate('due_date', '<', today()); } /** * Check if the task is overdue. */ public function isOverdue(): bool { return !$this->completed && $this->due_date && $this->due_date->isPast(); } }
New Concepts Explained
Attribute Casting
phpprotected function casts(): array { return [ 'completed' => 'boolean', // Casts 0/1 to true/false 'due_date' => 'date', // Casts to Carbon date object ]; }
Casting automatically converts database values to PHP types.
Query Scopes
Scopes are reusable query constraints:
php// Define scope public function scopeCompleted(Builder $query): Builder { return $query->where('completed', true); } // Use scope Task::completed()->get(); // All completed tasks Task::incomplete()->dueToday()->get(); // Incomplete tasks due today $user->tasks()->overdue()->get(); // User's overdue tasks
Custom Methods
phppublic function isOverdue(): bool { return !$this->completed && $this->due_date && $this->due_date->isPast(); } // Usage if ($task->isOverdue()) { // Show warning }
Updating the User Model
Add relationships to the existing User model. Edit app/Models/User.php:
php<?php namespace App\Models; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use Notifiable; protected $fillable = [ 'name', 'email', 'password', ]; protected $hidden = [ 'password', 'remember_token', ]; protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; } /** * Get the tasks for the user. */ public function tasks(): HasMany { return $this->hasMany(Task::class); } /** * Get the categories for the user. */ public function categories(): HasMany { return $this->hasMany(Category::class); } }
Testing Models with Tinker
Let's test our models:
bashphp artisan tinker
php// Create a test user first >>> $user = User::create(['name' => 'John', 'email' => 'john@example.com', 'password' => bcrypt('password')]) // Create a category >>> $category = $user->categories()->create(['name' => 'Work', 'color' => '#ef4444']) // Create tasks >>> $task = $user->tasks()->create([ ... 'title' => 'Complete Laravel course', ... 'description' => 'Finish all lessons and challenges', ... 'category_id' => $category->id, ... 'due_date' => now()->addDays(7), ... 'priority' => 'high' ... ]) // Query tasks >>> $user->tasks()->count() => 1 >>> $user->tasks()->incomplete()->get() => Tasks collection... >>> $task->category->name => "Work" >>> $task->isOverdue() => false
Relationship Summary
php// From User $user->tasks; // All user's tasks $user->tasks()->create(); // Create task for user $user->categories; // All user's categories // From Task $task->user; // Task's owner $task->category; // Task's category $task->isOverdue(); // Check if overdue // From Category $category->user; // Category's owner $category->tasks; // Tasks in this category
Now let's create the routes and controllers!
Common Pitfalls
- Skipping
$fillable— Forgetting it triggersMassAssignmentExceptionthe first time you callcreate(). - N+1 queries in loops —
foreach ($tasks as $task) { $task->user->name; }runs one query per task. Eager-load withTask::with('user').
Best Practices
- Put query logic in scopes —
Task::incomplete()->dueToday()beats copy-pasting where-clauses everywhere. - Use
$castsfor dates and booleans — You getCarbonand realboolvalues instead of strings.
Summary
- Eloquent models live under
app/Modelsand extendModel. $fillablecontrols mass assignment.- Relationships are declared as methods returning
HasMany,BelongsTo, etc. - Scopes (
scopeCompleted) wrap reusable query constraints. - Always eager-load relationships used in loops to avoid N+1 queries.