Laravel Eloquent ORM Guide
Eloquent ORM patterns: model definition, relationships, query scopes, accessors/mutators, eager loading, and mass assignment.
1. Model Definition
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Article extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'articles'; // explicit table name (optional)
protected $primaryKey = 'id';
public $timestamps = true;
// Fillable fields (whitelist for mass assignment)
protected $fillable = ['title', 'slug', 'content', 'status', 'author_id'];
// Hidden fields (excluded from JSON output)
protected $hidden = ['deleted_at'];
// Cast types
protected $casts = [
'published_at' => 'datetime',
'metadata' => 'array',
'is_featured' => 'boolean',
'view_count' => 'integer',
];
}
2. Relationships
class Article extends Model
{
// BelongsTo (many articles → one author)
public function author()
{
return $this->belongsTo(User::class, 'author_id');
}
// HasMany (one article → many comments)
public function comments()
{
return $this->hasMany(Comment::class)->latest();
}
// BelongsToMany (article ↔ tags)
public function tags()
{
return $this->belongsToMany(Tag::class, 'article_tags')
->withTimestamps()
->withPivot('order');
}
// HasOneThrough
public function authorProfile()
{
return $this->hasOneThrough(Profile::class, User::class,
'id', // users.id
'user_id', // profiles.user_id
'author_id', // articles.author_id
);
}
// Polymorphic
public function likes()
{
return $this->morphMany(Like::class, 'likeable');
}
}
3. Scopes
class Article extends Model
{
// Local scope — called as ->published()
public function scopePublished($query)
{
return $query->where('status', 'published');
}
// Parameterized scope
public function scopeByAuthor($query, $authorId)
{
return $query->where('author_id', $authorId);
}
// Global scope — always applied
protected static function booted()
{
static::addGlobalScope('active', function ($query) {
$query->where('is_active', true);
});
}
}
// Usage
$articles = Article::published()->byAuthor(1)->latest()->paginate(15);
$articles = Article::withoutGlobalScope('active')->get();
4. Accessors & Mutators
use Illuminate\Database\Eloquent\Casts\Attribute;
class Article extends Model
{
// Accessor — computed property
protected function readingTime(): Attribute
{
return Attribute::make(
get: fn () => ceil(str_word_count($this->content) / 200) . ' min',
);
}
// Mutator — transform on set
protected function title(): Attribute
{
return Attribute::make(
get: fn ($v) => ucfirst($v),
set: fn ($v) => trim($v),
);
}
}
// Access: $article->reading_time, $article->title
5. Eager Loading
// with() — eager load
$articles = Article::with(['author', 'tags', 'comments.user'])->published()->get();
// Constrained eager loading
$articles = Article::with([
'comments' => fn($q) => $q->where('approved', true)->latest()->take(5),
'tags:id,name,color',
])->get();
// withCount — adds {relation}_count attribute
$articles = Article::withCount(['comments', 'likes'])->get();
// Access: $article->comments_count
// Prevent N+1 globally
Model::preventLazyLoading(! app()->isProduction());
6. Query Builder Methods
| Method | Purpose |
|---|---|
| where / orWhere | Add WHERE conditions |
| whereIn / whereNotIn | IN clause |
| whereBetween | BETWEEN a AND b |
| whereNull / whereNotNull | IS NULL check |
| orderBy / latest / oldest | Sort results |
| select / addSelect | Column selection |
| groupBy / having | Aggregation |
| join / leftJoin | Raw JOIN |
| paginate / simplePaginate | Pagination |
| chunk / cursor | Memory-efficient iteration |