Alaa Amer Articles

We offer a comprehensive collection of essential educational articles in web development to turn your ideas into digital reality

Understanding MVC Pattern in Laravel: Architecture Mastery

Laravel 2026-01-01 Alaa Amer

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?

Laravel MVC Architecture Models Views Controllers Design Patterns Best Practices
Article Category
Laravel

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.

Understanding MVC Pattern in Laravel: Architecture Mastery
01

Consultation & Communication

Direct communication via WhatsApp or phone to understand your project needs precisely.

02

Planning & Scheduling

Creating clear work plan with specific timeline for each project phase.

03

Development & Coding

Building projects with latest technologies ensuring high performance and security.

04

Testing & Delivery

Comprehensive testing and thorough review before final project delivery.

Alaa Amer
Alaa Amer

Professional web developer with over 10 years of experience in building innovative digital solutions.

Need This Service?

Contact me now for a free consultation and quote

WhatsApp Your satisfaction is our ultimate goal

What We Offer

  • Website Maintenance & Updates

    Keep your website secure updated optimized

  • API Integration

    Connect your systems with powerful APIs

  • Database Design & Optimization

    Faster queries cleaner structure fewer issues

  • Website Security Hardening

    Protect your site from cyber threats

  • Automation & Scripts

    Automate repetitive tasks and save time

Have Questions?

Call Us Now

00201014714795