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 { ... }