LessonsBuild a Blog → Part 3

Models & Relationships

Laravel
⏱ 25 min read🏗️ ProjectNot completed

Now we'll set up all our Eloquent models with their relationships, accessors, mutators, and scopes. Good models make everything else much easier.

Post Model

app/Models/Post.php
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;

class Post extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'user_id', 'category_id', 'title', 'slug',
        'excerpt', 'body', 'image', 'published', 'published_at'
    ];

    protected $casts = [
        'published'    => 'boolean',
        'published_at' => 'datetime',
    ];

    // ---- RELATIONSHIPS ----

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

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

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

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

    public function approvedComments()
    {
        return $this->hasMany(Comment::class)->where('approved', true);
    }

    // ---- SCOPES ----

    // Post::published()->get()
    public function scopePublished($query)
    {
        return $query->where('published', true)
                     ->whereNotNull('published_at')
                     ->where('published_at', '<=', now());
    }

    // Post::latest()->get()  — already built in Laravel

    // Post::popular()->get()
    public function scopePopular($query)
    {
        return $query->orderBy('views', 'desc');
    }

    // Post::inCategory('laravel')->get()
    public function scopeInCategory($query, $slug)
    {
        return $query->whereHas('category', fn($q) => $q->where('slug', $slug));
    }

    // ---- ACCESSORS ----

    // $post->excerpt — auto-generate if empty
    public function getExcerptAttribute($value)
    {
        return $value ?: Str::limit(strip_tags($this->body), 200);
    }

    // $post->reading_time — calculated
    public function getReadingTimeAttribute()
    {
        $words = str_word_count(strip_tags($this->body));
        $minutes = ceil($words / 200); // avg reading speed
        return $minutes . ' min read';
    }

    // ---- MUTATORS ----

    // Auto-generate slug when title is set
    public function setTitleAttribute($value)
    {
        $this->attributes['title'] = $value;
        if (empty($this->attributes['slug'])) {
            $this->attributes['slug'] = Str::slug($value);
        }
    }
}

Category Model

app/Models/Category.php
<?php
class Category extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'slug', 'description', 'color'];

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

    public function publishedPosts()
    {
        return $this->hasMany(Post::class)->published();
    }

    // Count posts in category
    public function getPostCountAttribute()
    {
        return $this->publishedPosts()->count();
    }
}

Tag Model

app/Models/Tag.php
<?php
class Tag extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'slug'];

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

Comment Model

app/Models/Comment.php
<?php
class Comment extends Model
{
    use HasFactory;

    protected $fillable = [
        'post_id', 'user_id', 'author_name',
        'author_email', 'body', 'approved'
    ];

    protected $casts = ['approved' => 'boolean'];

    public function post()
    {
        return $this->belongsTo(Post::class);
    }

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

    // Get display name (user or guest)
    public function getDisplayNameAttribute()
    {
        return $this->user?->name ?? $this->author_name ?? 'Anonymous';
    }

    // Scope for approved comments only
    public function scopeApproved($query)
    {
        return $query->where('approved', true);
    }
}

Testing Relationships in Tinker

Terminal — php artisan tinker
# Open Laravel Tinker (interactive PHP console)
php artisan tinker

# Test your models
$post = Post::with(['user', 'category', 'tags'])->first();
$post->title;
$post->user->name;
$post->category->name;
$post->tags->pluck('name');
$post->reading_time;
$post->excerpt;

# Query with scopes
Post::published()->count();
Post::popular()->take(5)->get();
Post::inCategory('laravel')->get();
💡
Use Tinker constantly! php artisan tinker is your best friend for testing queries and relationships without writing a full controller. Just like you test APIs in Postman, use Tinker for database queries.
← Previous Next Lesson →
💡

Stuck? Need help?

Review the previous lessons or check the Laravel documentation.

Laravel Docs →