Laravel Eloquent Models: A Crash Course on Interacting with Your Database

Published on November 12, 2025
Laravel Eloquent Models ORM Database PHP

What are Eloquent Models?

Eloquent Models are the heart of Laravel's database interactions. Each model represents a single database table and provides an intuitive way to interact with that table's data. Think of models as intelligent representatives of your database tables that understand relationships, business logic, and data validation.

Basic Model Creation

# Create a model with migration
php artisan make:model Post -m

# Create model with migration and controller
php artisan make:model Post -mc

# Create model with migration, controller, and factory
php artisan make:model Post -mcf

# Create model in specific directory
php artisan make:model Models/Post

Model Structure and Configuration

Basic Model Example

<?php
// app/Models/Post.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Post extends Model
{
    use HasFactory;

    /**
     * The table associated with the model.
     */
    protected $table = 'posts';

    /**
     * The primary key for the model.
     */
    protected $primaryKey = 'id';

    /**
     * Indicates if the IDs are auto-incrementing.
     */
    public $incrementing = true;

    /**
     * The data type of the auto-incrementing ID.
     */
    protected $keyType = 'int';

    /**
     * Indicates if the model should be timestamped.
     */
    public $timestamps = true;

    /**
     * The attributes that are mass assignable.
     */
    protected $fillable = [
        'title', 'content', 'user_id', 'category_id'
    ];

    /**
     * The attributes that should be hidden for arrays.
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast.
     */
    protected $casts = [
        'is_published' => 'boolean',
        'published_at' => 'datetime',
        'metadata' => 'array',
    ];

    /**
     * The model's default values for attributes.
     */
    protected $attributes = [
        'views' => 0,
        'is_published' => false,
    ];
}

Mass Assignment Protection

Fillable vs Guarded

class User extends Model
{
    // ✅ RECOMMENDED: Explicitly allow mass assignment
    protected $fillable = [
        'name', 'email', 'password', 'role'
    ];

    // ❌ NOT RECOMMENDED: Allow all except specified
    protected $guarded = [
        'id', 'is_admin'
    ];

    // ⚠️ DANGEROUS: Allow all mass assignment
    protected $guarded = [];
}

// Usage examples
// ✅ This works with fillable
User::create([
    'name' => 'John',
    'email' => 'john@example.com',
    'password' => bcrypt('secret'),
    'role' => 'user'
]);

// ❌ This would fail with MassAssignmentException
User::create([
    'name' => 'John',
    'email' => 'john@example.com',
    'is_admin' => true // Not in fillable array
]);

Force Assignment (When Needed)

// Sometimes you need to bypass mass assignment protection
$user = new User;
$user->forceFill([
    'name' => 'Admin',
    'email' => 'admin@example.com',
    'is_admin' => true
]);
$user->save();

Attribute Casting

Common Cast Types

class Post extends Model
{
    protected $casts = [
        // Basic types
        'is_published' => 'boolean',
        'views' => 'integer',
        'rating' => 'float',
        'price' => 'decimal:2',
        
        // Date types
        'published_at' => 'datetime',
        'created_at' => 'datetime:Y-m-d H:i:s',
        'scheduled_for' => 'timestamp',
        
        // Array and JSON types
        'meta' => 'array',
        'settings' => 'object',
        'tags' => 'collection',
        
        // Special types
        'encrypted_data' => 'encrypted',
        'encrypted_array' => 'encrypted:array',
    ];
}

// Usage examples
$post = Post::find(1);

// Automatic casting in action
if ($post->is_published) { // Cast to boolean
    // Do something
}

$post->meta = ['key' => 'value']; // Automatically encoded to JSON
$post->save();

$decodedMeta = $post->meta; // Automatically decoded from JSON

Custom Casts

<?php
// app/Casts/MoneyCast.php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class MoneyCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes)
    {
        return $value / 100; // Convert cents to dollars
    }

    public function set($model, $key, $value, $attributes)
    {
        return $value * 100; // Convert dollars to cents
    }
}

// In your model
class Product extends Model
{
    protected $casts = [
        'price' => MoneyCast::class,
    ];
}

// Usage
$product = new Product;
$product->price = 19.99; // Stored as 1999 in database
echo $product->price; // Outputs 19.99

Accessors and Mutators

Defining Accessors (Getters)

class User extends Model
{
    // Accessor for existing column
    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }

    // Accessor for non-existing column (virtual)
    public function getFullNameAttribute()
    {
        return "{$this->first_name} {$this->last_name}";
    }

    // Accessor with conditional logic
    public function getProfileImageUrlAttribute()
    {
        if ($this->profile_image) {
            return asset("storage/profiles/{$this->profile_image}");
        }
        
        return asset("images/default-avatar.png");
    }

    // Date accessor
    public function getCreatedAtAttribute($value)
    {
        return Carbon::parse($value)->format('M j, Y');
    }
}

// Usage
$user = User::find(1);
echo $user->first_name; // Automatically formatted
echo $user->full_name;  // Virtual attribute
echo $user->profile_image_url; // Dynamic URL

Defining Mutators (Setters)

class User extends Model
{
    // Mutator for name
    public function setNameAttribute($value)
    {
        $this->attributes['name'] = trim($value);
    }

    // Mutator for email (always lowercase)
    public function setEmailAttribute($value)
    {
        $this->attributes['email'] = strtolower($value);
    }

    // Mutator for password (auto-hash)
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }

    // Mutator with multiple operations
    public function setSlugAttribute($value)
    {
        $this->attributes['slug'] = Str::slug($value);
        $this->attributes['slug_hash'] = md5(Str::slug($value));
    }

    // JSON mutator
    public function setSettingsAttribute($value)
    {
        $this->attributes['settings'] = json_encode($value);
    }
}

// Usage
$user = new User;
$user->name = '  John Doe  '; // Becomes 'John Doe'
$user->email = 'John@Example.COM'; // Becomes 'john@example.com'
$user->password = 'secret'; // Automatically hashed
$user->slug = 'My Great Post'; // Becomes 'my-great-post'
$user->save();

Query Scopes

Local Scopes

class Post extends Model
{
    // Basic scope
    public function scopePublished($query)
    {
        return $query->where('is_published', true);
    }

    // Scope with parameters
    public function scopeCategory($query, $categoryId)
    {
        return $query->where('category_id', $categoryId);
    }

    // Scope with multiple parameters
    public function scopeBetweenDates($query, $startDate, $endDate)
    {
        return $query->whereBetween('created_at', [$startDate, $endDate]);
    }

    // Scope with relationships
    public function scopeWithUser($query)
    {
        return $query->with('user');
    }

    // Dynamic scope
    public function scopeStatus($query, $status)
    {
        return $query->where('status', $status);
    }

    // Complex scope with multiple conditions
    public function scopePopular($query, $minViews = 1000, $minLikes = 100)
    {
        return $query->where('views', '>=', $minViews)
                    ->where('likes_count', '>=', $minLikes)
                    ->orderBy('views', 'desc');
    }
}

// Usage examples
$publishedPosts = Post::published()->get();
$techPosts = Post::category(1)->published()->get();
$popularPosts = Post::popular(5000, 200)->get();
$recentPosts = Post::betweenDates('2024-01-01', '2024-01-31')->get();

Global Scopes

<?php
// app/Scopes/ActiveScope.php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('is_active', true);
    }
}

// Applying global scope in model
class Post extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new ActiveScope);
        
        // Or use anonymous function
        static::addGlobalScope('active', function (Builder $builder) {
            $builder->where('is_active', true);
        });
    }
}

// Remove global scope for specific query
$allPosts = Post::withoutGlobalScope(ActiveScope::class)->get();
$allPosts = Post::withoutGlobalScopes()->get(); // Remove all

Model Relationships in Depth

One-to-One Relationship

class User extends Model
{
    public function profile()
    {
        return $this->hasOne(Profile::class);
        // return $this->hasOne(Profile::class, 'foreign_key', 'local_key');
    }
}

class Profile extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
        // return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
    }
}

// Usage
$user = User::find(1);
$profile = $user->profile;

$profileUser = $profile->user;

One-to-Many Relationship

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function publishedPosts()
    {
        return $this->hasMany(Post::class)->where('is_published', true);
    }
}

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function author()
    {
        return $this->belongsTo(User::class, 'author_id');
    }
}

// Usage examples
$user = User::find(1);

// Get all user's posts
$posts = $user->posts;

// Get only published posts
$publishedPosts = $user->publishedPosts;

// Create post for user
$user->posts()->create([
    'title' => 'New Post',
    'content' => 'Post content...'
]);

// Count relationships
$postCount = $user->posts()->count();

// Eager loading
$usersWithPosts = User::with('posts')->get();

Many-to-Many Relationship

class Post extends Model
{
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
        // return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id');
    }

    public function tagsWithPivot()
    {
        return $this->belongsToMany(Tag::class)
                    ->withPivot('created_at', 'added_by')
                    ->withTimestamps();
    }
}

class Tag extends Model
{
    public function posts()
    {
        return $this->belongsToMany(Post::class);
    }
}

// Usage examples
$post = Post::find(1);

// Attach tags
$post->tags()->attach(1);
$post->tags()->attach([1, 2, 3]);

// Detach tags
$post->tags()->detach(2);
$post->tags()->detach([1, 3]);

// Sync tags (only these tags will remain attached)
$post->tags()->sync([1, 2, 4]);

// Sync with pivot data
$post->tags()->sync([
    1 => ['added_by' => auth()->id()],
    2 => ['added_by' => auth()->id()],
]);

// Toggle tags
$post->tags()->toggle([1, 2, 3]);

// Access pivot data
foreach ($post->tags as $tag) {
    echo $tag->pivot->created_at;
}

Advanced Relationships

// Has Many Through
class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(Post::class, User::class);
    }
}

// Polymorphic Relationships
class Post extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }

    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

class Video extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Comment extends Model
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

// Usage
$post = Post::find(1);
$comments = $post->comments;

$video = Video::find(1);
$comments = $video->comments;

Model Events and Observers

Using Model Events

class Post extends Model
{
    protected static function booted()
    {
        // Creating - before create
        static::creating(function ($post) {
            $post->slug = Str::slug($post->title);
            $post->created_by = auth()->id();
        });

        // Created - after create
        static::created(function ($post) {
            // Send notifications, update cache, etc.
            Cache::forget('recent_posts');
        });

        // Updating - before update
        static::updating(function ($post) {
            $post->updated_by = auth()->id();
            $post->slug = Str::slug($post->title);
        });

        // Saving - before both create and update
        static::saving(function ($post) {
            $post->excerpt = Str::limit(strip_tags($post->content), 150);
        });

        // Saved - after both create and update
        static::saved(function ($post) {
            // Update search index, etc.
        });

        // Deleting - before delete
        static::deleting(function ($post) {
            // Delete related records
            $post->comments()->delete();
        });
    }
}

Using Observers (For Complex Logic)

php artisan make:observer PostObserver --model=Post
<?php
// app/Observers/PostObserver.php

namespace App\Observers;

use App\Models\Post;

class PostObserver
{
    public function creating(Post $post)
    {
        $post->slug = Str::slug($post->title);
        $post->created_by = auth()->id();
    }

    public function created(Post $post)
    {
        // Send notification to followers
        Notification::send($post->user->followers, new NewPostNotification($post));
        
        // Update cache
        Cache::forget('recent_posts');
    }

    public function updating(Post $post)
    {
        $post->slug = Str::slug($post->title);
        $post->updated_by = auth()->id();
    }

    public function saving(Post $post)
    {
        $post->excerpt = Str::limit(strip_tags($post->content), 150);
    }

    public function deleting(Post $post)
    {
        $post->comments()->delete();
        $post->tags()->detach();
    }
}

// Register observer in AppServiceProvider
public function boot()
{
    Post::observe(PostObserver::class);
}

Advanced Model Techniques

Soft Deletes

class Post extends Model
{
    use SoftDeletes;

    protected $dates = ['deleted_at'];

    // Custom soft delete column
    const DELETED_AT = 'deleted_date';
}

// Usage
$post = Post::find(1);
$post->delete(); // Sets deleted_at timestamp

// Include soft deleted records
$posts = Post::withTrashed()->get();

// Only soft deleted records
$deletedPosts = Post::onlyTrashed()->get();

// Restore soft deleted record
$post->restore();

// Force delete (permanently remove)
$post->forceDelete();

Querying Relationship Existence

// Get users who have posts
$usersWithPosts = User::has('posts')->get();

// Get users with more than 5 posts
$activeUsers = User::has('posts', '>=', 5)->get();

// Get users with published posts
$usersWithPublishedPosts = User::whereHas('posts', function ($query) {
    $query->where('is_published', true);
})->get();

// Get users without posts
$usersWithoutPosts = User::doesntHave('posts')->get();

// Count related models
$users = User::withCount('posts')->get();
foreach ($users as $user) {
    echo $user->posts_count;
}

// Multiple counts with conditions
$users = User::withCount([
    'posts',
    'posts as published_posts_count' => function ($query) {
        $query->where('is_published', true);
    }
])->get();

Model Serialization

class User extends Model
{
    // Append accessors to JSON
    protected $appends = ['full_name', 'profile_url'];

    // Hide attributes from JSON
    protected $hidden = ['password', 'remember_token'];

    // Or use visible to specify only allowed attributes
    protected $visible = ['id', 'name', 'email'];

    public function toArray()
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'profile' => $this->profile,
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

// Usage
$user = User::find(1);
return response()->json($user);

// Serialize with relationships
$user->load('posts', 'profile');
return $user->toJson();

Practical Complete Example

Blog Post Model with Full Features

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Carbon\Carbon;

class Post extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'title', 'slug', 'content', 'excerpt', 'featured_image',
        'is_published', 'published_at', 'user_id', 'category_id',
        'views', 'meta_title', 'meta_description'
    ];

    protected $casts = [
        'is_published' => 'boolean',
        'published_at' => 'datetime',
        'views' => 'integer',
        'metadata' => 'array',
    ];

    protected $attributes = [
        'views' => 0,
        'is_published' => false,
    ];

    protected $appends = ['reading_time', 'published_ago'];

    // Relationships
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)->withTimestamps();
    }

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }

    // Scopes
    public function scopePublished($query)
    {
        return $query->where('is_published', true)
                    ->where('published_at', '<=', now());
    }

    public function scopeDraft($query)
    {
        return $query->where('is_published', false)
                    ->orWhereNull('published_at');
    }

    public function scopePopular($query, $minViews = 1000)
    {
        return $query->where('views', '>=', $minViews);
    }

    public function scopeRecent($query, $days = 7)
    {
        return $query->where('created_at', '>=', now()->subDays($days));
    }

    public function scopeByCategory($query, $categoryId)
    {
        return $query->where('category_id', $categoryId);
    }

    public function scopeSearch($query, $searchTerm)
    {
        return $query->where(function ($q) use ($searchTerm) {
            $q->where('title', 'like', "%{$searchTerm}%")
              ->orWhere('content', 'like', "%{$searchTerm}%");
        });
    }

    // Accessors
    public function getReadingTimeAttribute(): int
    {
        $wordCount = str_word_count(strip_tags($this->content));
        return max(1, ceil($wordCount / 200)); // 200 words per minute
    }

    public function getPublishedAgoAttribute(): string
    {
        if (!$this->published_at) {
            return 'Not published';
        }

        return $this->published_at->diffForHumans();
    }

    public function getExcerptAttribute($value): string
    {
        return $value ?: Str::limit(strip_tags($this->content), 150);
    }

    public function getFeaturedImageUrlAttribute(): string
    {
        if ($this->featured_image) {
            return asset("storage/posts/{$this->featured_image}");
        }

        return asset('images/default-post.jpg');
    }

    // Mutators
    public function setTitleAttribute($value): void
    {
        $this->attributes['title'] = $value;
        $this->attributes['slug'] = Str::slug($value);
    }

    public function setPublishedAtAttribute($value): void
    {
        $this->attributes['published_at'] = $value;
        $this->attributes['is_published'] = !is_null($value);
    }

    // Business Logic Methods
    public function publish(): bool
    {
        return $this->update([
            'is_published' => true,
            'published_at' => $this->published_at ?: now(),
        ]);
    }

    public function unpublish(): bool
    {
        return $this->update([
            'is_published' => false,
            'published_at' => null,
        ]);
    }

    public function incrementViews(): void
    {
        $this->increment('views');
    }

    public function isOwnedBy(User $user): bool
    {
        return $this->user_id === $user->id;
    }

    public function addTag($tagId): void
    {
        $this->tags()->syncWithoutDetaching([$tagId]);
    }

    // Model Events
    protected static function booted(): void
    {
        static::creating(function ($post) {
            if (empty($post->user_id) && auth()->check()) {
                $post->user_id = auth()->id();
            }
        });

        static::saving(function ($post) {
            if (empty($post->excerpt)) {
                $post->excerpt = Str::limit(strip_tags($post->content), 150);
            }

            // Generate meta fields if empty
            if (empty($post->meta_title)) {
                $post->meta_title = $post->title;
            }
            if (empty($post->meta_description)) {
                $post->meta_description = $post->excerpt;
            }
        });

        static::deleting(function ($post) {
            if (!$post->isForceDeleting()) {
                $post->comments()->delete();
            }
        });
    }
}

// Usage Examples
$post = Post::create([
    'title' => 'My New Blog Post',
    'content' => 'This is the content of my blog post...',
    'category_id' => 1,
]);

$post->publish();
$post->addTag(1);
$post->incrementViews();

// Complex queries
$popularTechPosts = Post::published()
                       ->byCategory(1) // Technology category
                       ->popular(5000)
                       ->with(['user', 'category', 'tags'])
                       ->orderBy('views', 'desc')
                       ->take(10)
                       ->get();

Common Interview Questions & Answers

1. What's the difference between fillable and guarded?

fillable specifies which attributes can be mass assigned, while guarded specifies which attributes cannot be mass assigned. It's recommended to use fillable for explicit control.

2. When would you use accessors vs mutators?

Use accessors to format attribute values when retrieving from the database, and mutators to format attribute values before saving to the database.

3. What are query scopes and why are they useful?

Query scopes are reusable query constraints that can be chained onto Eloquent queries. They help keep code DRY and make complex queries more readable.

4. How do model events differ from observers?

Model events are defined directly in the model class and are good for simple logic. Observers are separate classes that handle more complex event logic and keep the model clean.

5. What's the purpose of attribute casting?

Attribute casting automatically converts attributes to common data types (boolean, integer, array, etc.) when accessing them, eliminating the need for manual type conversion.

6. How do you handle soft deletes?

Use the SoftDeletes trait and deleted_at column. Soft deleted records aren't removed but are excluded from normal queries. Use withTrashed(), onlyTrashed(), and restore() to work with them.

Best Practices

  • Always define $fillable for mass assignment protection
  • Use type hints for relationships and method returns
  • Keep models focused on data access logic
  • Use eager loading to prevent N+1 query problems
  • Implement business logic in model methods
  • Use accessors/mutators for data formatting
  • Create query scopes for reusable query logic
  • Handle complex events in observers

You've now mastered Laravel Eloquent Models and can efficiently interact with your database using elegant, object-oriented syntax! In our next post, we'll explore Laravel Tinker: The Powerful Tool for Testing Your Laravel Code Quickly to learn how to test and experiment with your models and application logic.