Get 20% off web development packages
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.
Services Related to This Article
All ServicesWant to apply this article to your project?
If this topic is relevant to your current project, you can jump to one of the services above or browse the services page to choose the most suitable solution.