Laravel

Eloquent Models

Complete Eloquent reference: model configuration (fillable, guarded, casting), all relationship types (hasOne, hasMany, belongsTo, belongsToMany, polymorphic), local and global scopes, modern accessors/mutators, and model events/observers.

Model Creation: Generate a model with php artisan make:model Post. Add -m for migration, -f for factory, -s for seeder, -c for controller, or -a for all at once. Configure table, primary key, and mass assignment protection inside the class.
📄terminal & app/Models/Post.php
PHP
# Generate model + migration + factory + seeder + controller + policy
php artisan make:model Post -a

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

namespace App\Models;

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

class Post extends Model
{
    use HasFactory, SoftDeletes;

    // ── Table Configuration ───────────────────
    protected $table      = 'posts';        // default: plural snake_case of class name
    protected $primaryKey = 'id';           // default: 'id'
    public    $incrementing = true;          // set false for UUID/ULID PKs
    protected $keyType    = 'int';           // 'string' for UUID/ULID
    public    $timestamps  = true;           // manages created_at & updated_at
    protected $dateFormat  = 'U';           // Unix timestamp format (optional)

    // ── Custom Date Column Names (optional) ───
    const CREATED_AT = 'created_at';
    const UPDATED_AT = 'updated_at';

    // ── Mass Assignment ───────────────────────
    // Option A: Allow specific columns (preferred — more secure)
    protected $fillable = [
        'user_id',
        'category_id',
        'title',
        'slug',
        'excerpt',
        'body',
        'status',
        'is_featured',
        'rating',
        'published_at',
        'meta',
    ];

    // Option B: Guard specific columns (allow all others)
    // protected $guarded = ['id'];

    // ── Attribute Casting ─────────────────────
    protected $casts = [
        'is_featured'  => 'boolean',
        'rating'       => 'float',
        'meta'         => 'array',        // JSON <-> PHP array
        'published_at' => 'datetime',
        'status'       => \App\Enums\PostStatus::class,  // PHP 8.1 backed enum
    ];

    // ── Hidden & Visible ──────────────────────
    protected $hidden   = ['user_id', 'deleted_at'];  // exclude from serialization
    protected $visible  = [];                          // whitelist (empty = all)

    // ── Append Custom Accessors ───────────────
    protected $appends  = ['reading_time', 'is_new'];
}
Relationships (One-to-One / One-to-Many / Many-to-Many): Eloquent relationships are defined as methods on the model. hasOne and hasMany are for owning side; belongsTo for the owned side. belongsToMany uses a pivot table.
📄app/Models/User.php & Post.php – Relationships
PHP
<?php
// ─── app/Models/User.php ──────────────────────────────

class User extends Authenticatable
{
    // One user has ONE profile (hasOne)
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
        // Eloquent assumes Profile.user_id as FK
        // Custom FK: hasOne(Profile::class, 'owner_id', 'id')
    }

    // One user has MANY posts (hasOne)
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    // HasMany through another model
    // User -> Post -> Comment  (hasManyThrough)
    public function postComments(): HasManyThrough
    {
        return $this->hasManyThrough(Comment::class, Post::class);
    }

    // Many users belong to many roles (belongsToMany)
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class)
                     ->withPivot('assigned_at')     // include pivot columns
                     ->withTimestamps()              // pivot has created_at/updated_at
                     ->as('assignment')              // rename pivot accessor: $user->roles[0]->assignment->assigned_at
                     ->wherePivotNull('revoked_at'); // filter via pivot
    }
}

// ─── app/Models/Post.php ──────────────────────────────

class Post extends Model
{
    // Post belongs to one user (belongsTo)
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
        // Custom FK: belongsTo(User::class, 'author_id', 'id')
    }

    // Post belongs to one category
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    // Post has many comments
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class)->latest();
    }

    // Post has ONE latest comment
    public function latestComment(): HasOne
    {
        return $this->hasOne(Comment::class)->latestOfMany();
    }

    // Post belongs to many tags (pivot: post_tag)
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id')
                     ->withTimestamps();
    }
}

// ─────────────────────────────────────────────
// Usage examples
// ─────────────────────────────────────────────
// Eager loading (prevents N+1)
$posts = Post::with(['user', 'category', 'tags'])->get();
$users = User::with('posts.comments')->get();

// Lazy eager loading
$post->load('comments.user');

// Accessing relationship data
$post->user->name;
$user->posts->count();
$post->tags->pluck('name');

// Attaching/detaching pivot records (belongsToMany)
$post->tags()->attach([1, 2, 3]);
$post->tags()->detach(1);
$post->tags()->sync([1, 3, 5]);   // removes old, adds new
$post->tags()->toggle([2, 4]);     // add if absent, remove if present

// Creating related models
$user->posts()->create(['title' => 'New Post', 'slug' => 'new-post', 'body' => '...']);
$post->comments()->saveMany([new Comment(['body' => 'Great!']), new Comment(['body' => 'Nice!'])]);
Polymorphic Relationships: A polymorphic relation allows a model to belong to more than one other model using a single association. Uses *_type and *_id columns to store the related model class and ID. Great for comments, likes, images, tags that apply to multiple models.
📄app/Models/Comment.php, Post.php, Video.php
PHP
<?php
// Migration for comments (polymorphic)
// $table->morphs('commentable'); // adds commentable_id (bigint) + commentable_type (string)

// ─── app/Models/Comment.php ──────────────────────────
class Comment extends Model
{
    protected $fillable = ['user_id', 'body'];

    // Belongs to either Post or Video (or any morphable model)
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }

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

// ─── app/Models/Post.php ─────────────────────────────
class Post extends Model
{
    // A post has many comments (polymorphic)
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }

    // A post has one featured image (polymorphic)
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }

    // A post belongs to many tags (morphToMany)
    public function tags(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

// ─── app/Models/Tag.php ──────────────────────────────
class Tag extends Model
{
    // Tag morphed by many posts
    public function posts(): MorphedByMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    // Tag morphed by many videos
    public function videos(): MorphedByMany
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

// ─────────────────────────────────────────────
// Usage
// ─────────────────────────────────────────────
// Add a comment to a post
$post->comments()->create(['user_id' => 1, 'body' => 'Great post!']);

// Retrieve parent from a comment
$comment = Comment::find(1);
$commentable = $comment->commentable; // returns Post or Video instance

// Constrain eager loads
$posts = Post::with(['comments' => function ($query) {
    $query->latest()->limit(5);
}])->get();

// Custom morph map (use short names instead of FQCN)
// In AppServiceProvider::boot():
// Relation::morphMap([
//     'post'  => \App\Models\Post::class,
//     'video' => \App\Models\Video::class,
// ]);
Query Scopes: Local scopes are defined with a scope prefix and called without the prefix on the query builder. Global scopes automatically apply to every query on the model — great for soft-delete-style filtering.
📄app/Models/Post.php – Scopes
PHP
<?php
// ─── Local Scopes ────────────────────────────────────

class Post extends Model
{
    // Filter published posts
    public function scopePublished($query)
    {
        return $query->where('status', 'published')
                     ->whereNotNull('published_at');
    }

    // Filter by status (parameterized scope)
    public function scopeOfStatus($query, string $status)
    {
        return $query->where('status', $status);
    }

    // Filter featured posts
    public function scopeFeatured($query)
    {
        return $query->where('is_featured', true);
    }

    // Filter posts by minimum rating
    public function scopeMinRating($query, float $min = 3.0)
    {
        return $query->where('rating', '>=', $min);
    }

    // Recent posts (within N days)
    public function scopeRecent($query, int $days = 7)
    {
        return $query->where('published_at', '>=', now()->subDays($days));
    }

    // Popular = many comments
    public function scopePopular($query)
    {
        return $query->withCount('comments')
                     ->orderByDesc('comments_count');
    }
}

// Usage (chain scopes fluently):
$posts = Post::published()->featured()->recent(30)->get();
$popular = Post::published()->popular()->paginate(10);
$drafts  = Post::ofStatus('draft')->latest()->get();
$topRated = Post::published()->minRating(4.5)->get();

// ─── Global Scopes ───────────────────────────────────
// Define in a separate class:

namespace App\Models\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): void
    {
        $builder->where('is_active', true);
    }
}

// Apply in Post model:
// use App\Models\Scopes\ActiveScope;
// protected static function booted(): void
// {
//     static::addGlobalScope(new ActiveScope());
//
//     // Or as a closure:
//     static::addGlobalScope('active', fn (Builder $builder) =>
//         $builder->where('is_active', true)
//     );
// }

// Remove global scope for a query:
// Post::withoutGlobalScope(ActiveScope::class)->get();
// Post::withoutGlobalScopes()->get(); // remove all global scopes
Accessors & Mutators (Laravel 9+ syntax): Use Illuminate\Database\Eloquent\Casts\Attribute with get (accessor) and set (mutator) closures. Accessors transform data when reading; mutators transform data when writing to the model.
📄app/Models/Post.php – Accessors & Mutators
PHP
<?php

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Str;

class Post extends Model
{
    protected $appends = ['reading_time', 'is_new', 'excerpt_short'];

    // ── Accessor only ─────────────────────────────────
    // Estimated reading time in minutes
    protected function readingTime(): Attribute
    {
        return Attribute::make(
            get: fn () => (int) ceil(str_word_count($this->body) / 200),
        );
    }

    // Accessor: is post newer than 3 days?
    protected function isNew(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->created_at?->gt(now()->subDays(3)) ?? false,
        );
    }

    // Truncated excerpt for cards
    protected function excerptShort(): Attribute
    {
        return Attribute::make(
            get: fn () => Str::limit($this->excerpt ?? $this->body, 120),
        );
    }

    // ── Mutator only ──────────────────────────────────
    // Auto-generate slug from title on set
    protected function title(): Attribute
    {
        return Attribute::make(
            set: function (string $value) {
                return [
                    'title' => $value,
                    'slug'  => Str::slug($value),  // set two columns at once
                ];
            }
        );
    }

    // ── Accessor + Mutator ────────────────────────────
    // Always store name in uppercase; retrieve in Title Case
    protected function authorName(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => Str::title($value),
            set: fn (string $value) => strtolower($value),
        )->shouldCache(); // cache computed value within same request
    }

    // Store password as hash automatically
    protected function password(): Attribute
    {
        return Attribute::make(
            set: fn (string $value) => bcrypt($value),
        );
    }

    // Format price for display (e.g., "Rp 25.000")
    protected function formattedPrice(): Attribute
    {
        return Attribute::make(
            get: fn () => 'Rp ' . number_format($this->price, 0, ',', '.'),
        );
    }
}

// Usage:
// $post->reading_time;      // computed minutes (accessor)
// $post->is_new;            // bool (accessor)
// $post->excerpt_short;     // truncated string
// $post->title = 'Hello!';  // also sets slug automatically (mutator)
// $post->formatted_price;   // 'Rp 25.000'
Model Events & Observers: Eloquent fires events during the model lifecycle: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored. Use the booted() method for quick hooks or an Observer class for organized logic.
📄app/Models/Post.php & app/Observers/PostObserver.php
PHP
<?php
// ─── Model events via booted() ───────────────────────

class Post extends Model
{
    use SoftDeletes;

    protected static function booted(): void
    {
        // Before creating: auto-set user_id and slug
        static::creating(function (Post $post) {
            $post->user_id = auth()->id() ?? $post->user_id;

            if (empty($post->slug)) {
                $post->slug = Str::slug($post->title);
            }
        });

        // After creation: clear cache, send notification
        static::created(function (Post $post) {
            cache()->tags(['posts'])->flush();
            // Notification::send($post->user, new PostCreated($post));
        });

        // Before updating: re-generate slug if title changed
        static::updating(function (Post $post) {
            if ($post->isDirty('title')) {
                $post->slug = Str::slug($post->title);
            }
        });

        // After soft-delete: clean up related records
        static::deleted(function (Post $post) {
            $post->comments()->delete();
        });

        // After restore: restore comments
        static::restored(function (Post $post) {
            $post->comments()->restore();
        });
    }
}

// ─── Observer class (preferred for complex logic) ────
// Generate: php artisan make:observer PostObserver --model=Post

namespace App\Observers;

use App\Models\Post;
use Illuminate\Support\Facades\Cache;

class PostObserver
{
    public function creating(Post $post): void
    {
        $post->user_id = auth()->id() ?? $post->user_id;
    }

    public function created(Post $post): void
    {
        Cache::tags(['posts'])->flush();
    }

    public function updating(Post $post): void
    {
        if ($post->isDirty('status') && $post->status === 'published') {
            $post->published_at ??= now();
        }
    }

    public function updated(Post $post): void
    {
        Cache::forget('post_' . $post->id);
    }

    public function deleted(Post $post): void
    {
        $post->comments()->delete();
    }

    public function restored(Post $post): void
    {
        $post->comments()->withTrashed()->restore();
    }
}

// Register observer in AppServiceProvider::boot():
// Post::observe(PostObserver::class);

// Or use the #[ObservedBy] attribute on the model (Laravel 10+):
// #[ObservedBy([PostObserver::class])]
// class Post extends Model { ... }