Understanding MVC Pattern in Laravel: Architecture Mastery
Understanding MVC Pattern in Laravel: Architecture Mastery
Expert Guide by Alaa Amer – Professional Web Developer & Applications Designer
The MVC (Model-View-Controller) pattern is the backbone of Laravel's architecture. Master this pattern to build maintainable, scalable, and professional applications.
2️⃣ Models: The Data Powerhouse
Advanced Model Implementation:
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, BelongsToMany};
class Post extends Model
{
use HasFactory, SoftDeletes;
// === MASS ASSIGNMENT PROTECTION ===
protected $fillable = [
'title', 'slug', 'content', 'excerpt', 'featured_image',
'status', 'published_at', 'user_id', 'category_id'
];
protected $guarded = ['id', 'created_at', 'updated_at'];
// === DATA CASTING ===
protected $casts = [
'published_at' => 'datetime',
'meta' => 'array',
'is_featured' => 'boolean',
'settings' => 'json'
];
// === RELATIONSHIPS ===
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)
->withTimestamps()
->withPivot(['added_by']);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function approvedComments(): HasMany
{
return $this->comments()->where('status', 'approved');
}
// === QUERY SCOPES ===
public function scopePublished($query)
{
return $query->where('status', 'published')
->where('published_at', '<=', now());
}
public function scopeFeatured($query)
{
return $query->where('is_featured', true);
}
public function scopeByAuthor($query, $authorId)
{
return $query->where('user_id', $authorId);
}
public function scopeInCategory($query, $categorySlug)
{
return $query->whereHas('category', function($q) use ($categorySlug) {
$q->where('slug', $categorySlug);
});
}
public function scopePopular($query, $timeframe = 30)
{
return $query->withCount(['comments', 'likes'])
->where('created_at', '>=', now()->subDays($timeframe))
->orderBy('likes_count', 'desc')
->orderBy('comments_count', 'desc');
}
// === ACCESSORS (Getters) ===
public function getTitleAttribute($value)
{
return ucfirst($value);
}
public function getExcerptAttribute($value)
{
return $value ?: Str::limit(strip_tags($this->content), 150);
}
public function getReadingTimeAttribute()
{
$wordCount = str_word_count(strip_tags($this->content));
return ceil($wordCount / 200); // Average 200 words per minute
}
public function getIsPublishedAttribute()
{
return $this->status === 'published' &&
$this->published_at &&
$this->published_at->isPast();
}
public function getUrlAttribute()
{
return route('posts.show', $this->slug);
}
// === MUTATORS (Setters) ===
public function setTitleAttribute($value)
{
$this->attributes['title'] = $value;
$this->attributes['slug'] = Str::slug($value);
}
public function setContentAttribute($value)
{
$this->attributes['content'] = $value;
// Auto-calculate reading time
$wordCount = str_word_count(strip_tags($value));
$this->attributes['reading_time'] = ceil($wordCount / 200);
}
// === BUSINESS LOGIC METHODS ===
public function publish()
{
$this->update([
'status' => 'published',
'published_at' => now()
]);
}
public function unpublish()
{
$this->update(['status' => 'draft']);
}
public function incrementViews()
{
$this->increment('views_count');
}
public function isOwnedBy(User $user)
{
return $this->user_id === $user->id;
}
public function canBeEditedBy(User $user)
{
return $this->isOwnedBy($user) || $user->hasRole('editor');
}
public function getRelatedPosts($limit = 4)
{
return static::published()
->where('category_id', $this->category_id)
->where('id', '!=', $this->id)
->inRandomOrder()
->limit($limit)
->get();
}
// === MODEL EVENTS ===
protected static function boot()
{
parent::boot();
static::creating(function ($post) {
if (empty($post->slug)) {
$post->slug = Str::slug($post->title);
}
});
static::updating(function ($post) {
if ($post->isDirty('title')) {
$post->slug = Str::slug($post->title);
}
});
static::deleting(function ($post) {
// Clean up related data
$post->comments()->delete();
$post->tags()->detach();
});
}
}
// === SEPARATE BUSINESS LOGIC INTO SERVICES ===
// app/Services/PostService.php
namespace App\Services;
class PostService
{
public function createPost(array $data, User $author)
{
$post = new Post($data);
$post->user_id = $author->id;
$post->save();
// Handle tags
if (isset($data['tags'])) {
$post->tags()->sync($data['tags']);
}
// Handle featured image
if (isset($data['featured_image'])) {
$post->addMediaFromRequest('featured_image')
->toMediaCollection('featured');
}
return $post;
}
public function getPopularPosts($timeframe = 7, $limit = 10)
{
return Post::published()
->popular($timeframe)
->with(['author', 'category'])
->limit($limit)
->get();
}
}
4️⃣ Controllers: Request Orchestrators
Professional Controller Implementation:
<?php
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\{Post, Category};
use App\Services\PostService;
use App\Http\Requests\{StorePostRequest, UpdatePostRequest};
use Illuminate\Http\{Request, RedirectResponse};
use Illuminate\View\View;
class PostController extends Controller
{
public function __construct(
private PostService $postService
) {
$this->middleware('auth')->except(['index', 'show']);
$this->middleware('verified')->only(['create', 'store']);
// Apply rate limiting
$this->middleware('throttle:60,1')->only(['store', 'update']);
}
/**
* Display paginated list of posts
*/
public function index(Request $request): View
{
$query = Post::with(['author:id,name,avatar', 'category:id,name,slug'])
->withCount(['comments', 'likes'])
->published();
// Apply filters
$query = $this->applyFilters($query, $request);
// Apply search
if ($request->filled('search')) {
$query->where(function($q) use ($request) {
$searchTerm = $request->search;
$q->where('title', 'LIKE', "%{$searchTerm}%")
->orWhere('content', 'LIKE', "%{$searchTerm}%");
});
}
// Sort options
$sort = $request->get('sort', 'latest');
match($sort) {
'popular' => $query->orderBy('likes_count', 'desc'),
'oldest' => $query->oldest('published_at'),
default => $query->latest('published_at')
};
$posts = $query->paginate(12)->withQueryString();
return view('posts.index', [
'posts' => $posts,
'categories' => Category::withCount('posts')->get(),
'currentFilters' => $request->only(['category', 'search', 'sort'])
]);
}
/**
* Show individual post
*/
public function show(Post $post): View
{
// Security check
abort_unless(
$post->isPublished() || $post->canBeEditedBy(auth()->user() ?? new User()),
404
);
// Eager load relationships
$post->load([
'author:id,name,email,avatar,bio',
'category:id,name,slug',
'tags:id,name,slug',
'comments' => fn($query) => $query->approved()
->with('user:id,name,avatar')
->latest()
]);
// Get related posts
$relatedPosts = $post->getRelatedPosts(4);
// Track view (async job recommended)
$post->incrementViews();
return view('posts.show', compact('post', 'relatedPosts'));
}
/**
* Show create form
*/
public function create(): View
{
$this->authorize('create', Post::class);
return view('posts.create', [
'categories' => Category::active()->orderBy('name')->get(),
'post' => new Post() // For form model binding
]);
}
/**
* Store new post
*/
public function store(StorePostRequest $request): RedirectResponse
{
$this->authorize('create', Post::class);
$post = $this->postService->createPost(
$request->validated(),
$request->user()
);
return redirect()
->route('posts.show', $post)
->with('success', 'Post created successfully!');
}
/**
* Show edit form
*/
public function edit(Post $post): View
{
$this->authorize('update', $post);
return view('posts.edit', [
'post' => $post,
'categories' => Category::active()->orderBy('name')->get()
]);
}
/**
* Update existing post
*/
public function update(UpdatePostRequest $request, Post $post): RedirectResponse
{
$this->authorize('update', $post);
$this->postService->updatePost($post, $request->validated());
return redirect()
->route('posts.show', $post)
->with('success', 'Post updated successfully!');
}
/**
* Delete post
*/
public function destroy(Post $post): RedirectResponse
{
$this->authorize('delete', $post);
$post->delete();
return redirect()
->route('posts.index')
->with('success', 'Post deleted successfully!');
}
/**
* Toggle like status
*/
public function toggleLike(Post $post)
{
$user = auth()->user();
if ($post->isLikedBy($user)) {
$post->unlike($user);
$liked = false;
} else {
$post->like($user);
$liked = true;
}
return response()->json([
'liked' => $liked,
'likes_count' => $post->likes()->count()
]);
}
/**
* Apply filters to query
*/
private function applyFilters($query, Request $request)
{
if ($request->filled('category')) {
$query->whereHas('category', function($q) use ($request) {
$q->where('slug', $request->category);
});
}
if ($request->filled('tag')) {
$query->whereHas('tags', function($q) use ($request) {
$q->where('slug', $request->tag);
});
}
return $query;
}
}
// === FORM REQUEST VALIDATION ===
// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->check();
}
public function rules(): array
{
return [
'title' => 'required|string|max:255|unique:posts,title',
'content' => 'required|string|min:100',
'excerpt' => 'nullable|string|max:500',
'category_id' => 'required|exists:categories,id',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'featured_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
'status' => 'required|in:draft,published',
'published_at' => 'nullable|date|after_or_equal:now'
];
}
public function messages(): array
{
return [
'title.unique' => 'A post with this title already exists.',
'content.min' => 'Post content must be at least 100 characters.',
'featured_image.max' => 'Image size should not exceed 2MB.'
];
}
}
💡 MVC Best Practices
1. Fat Models, Skinny Controllers
// ❌ Bad - Business logic in controller
class PostController extends Controller
{
public function publish(Post $post)
{
$post->status = 'published';
$post->published_at = now();
$post->save();
// Send notifications
$post->author->notify(new PostPublishedNotification($post));
return redirect()->back();
}
}
// ✅ Good - Business logic in model/service
class Post extends Model
{
public function publish()
{
$this->update([
'status' => 'published',
'published_at' => now()
]);
$this->author->notify(new PostPublishedNotification($this));
return $this;
}
}
class PostController extends Controller
{
public function publish(Post $post)
{
$post->publish();
return redirect()->back()->with('success', 'Post published!');
}
}
2. Single Responsibility Principle
// ❌ Bad - Controller doing too much
class PostController extends Controller
{
public function store(Request $request)
{
// Validation
$validated = $request->validate([...]);
// Image processing
if ($request->hasFile('image')) {
$path = $request->file('image')->store('posts');
// Resize image
// Create thumbnails
// Optimize image
}
// Create post
$post = Post::create($validated);
// Send notifications
// Update search index
// Clear caches
return redirect()->route('posts.show', $post);
}
}
// ✅ Good - Separated responsibilities
class PostController extends Controller
{
public function store(StorePostRequest $request, PostService $service)
{
$post = $service->createPost($request->validated());
return redirect()->route('posts.show', $post);
}
}
class PostService
{
public function __construct(
private ImageService $imageService,
private NotificationService $notificationService
) {}
public function createPost(array $data): Post
{
if (isset($data['image'])) {
$data['featured_image'] = $this->imageService->process($data['image']);
}
$post = Post::create($data);
$this->notificationService->notifyPostCreated($post);
return $post;
}
}
Next Steps
Ready to enhance your views? Learn Blade Templates Engine for powerful, maintainable frontend development.
📩 Need help with MVC architecture?
Article Category
Understanding MVC Pattern in Laravel: Architecture Mastery
Master the Model-View-Controller pattern in Laravel with practical examples, best practices, and advanced architectural concepts for scalable applications.
Consultation & Communication
Direct communication via WhatsApp or phone to understand your project needs precisely.
Planning & Scheduling
Creating clear work plan with specific timeline for each project phase.
Development & Coding
Building projects with latest technologies ensuring high performance and security.
Testing & Delivery
Comprehensive testing and thorough review before final project delivery.