MVC — Model, View, Controller in the context of Laravel
Concept
MVC stands for Model-View-Controller, a pattern coined by Trygve Reenskaug in the 1970s for Smalltalk GUIs. The original idea was clear: the Model holds domain state, the View observes the Model and renders it, and the Controller handles user input and updates the Model. Views were active — they subscribed to Model changes and re-rendered themselves. This is nothing like what most web frameworks do.
Laravel's version is better described as a passive-MVC or MVP hybrid. The View is a dumb template (Blade) that receives data pushed to it by the Controller. There is no direct Model-to-View observation. The Controller fetches data from the Model layer (Eloquent), transforms it, and passes it into the View. The View is entirely passive and has no awareness of the Model. This is actually closer to the Model-View-Presenter pattern, but the industry settled on calling it MVC, so we live with the inaccuracy.
The practical implication is that your Controller becomes the hub of orchestration. A thin controller receives the HTTP request, delegates to services or repositories for business logic, and returns a response. A fat controller does all of that inline — validating input, querying the database, sending emails, and returning a response — which quickly becomes unmaintainable. The golden rule: if your controller method exceeds 20 lines, business logic has leaked in.
Laravel's Eloquent model is also not a pure Domain Model — it is an Active Record, meaning the model object knows how to persist itself. This couples your domain logic to your database schema. For simple CRUD apps this is fine. For complex domains, it creates friction. Knowing this distinction helps you decide when to reach for additional patterns like Repositories or Domain Models.
| Pattern | View awareness | Controller role | Model type |
|---|---|---|---|
| Original MVC | Active (observes Model) | Handles input only | Domain object |
| Laravel MVC | Passive (receives data) | Orchestrates + transforms | Active Record (Eloquent) |
| MVP | Passive | Presenter mediates all | Separate domain model |
| MVVM | Reactive (data-binding) | Minimal or none | ViewModel |
Code Example
<?php
declare(strict_types=1);
// app/Http/Controllers/PostController.php — thin controller
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use App\Services\PostPublisher;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class PostController extends Controller
{
public function __construct(
private readonly PostPublisher $publisher,
) {}
// Controller responsibility: receive request, delegate, return response.
// No business logic here — just orchestration.
public function store(StorePostRequest $request): RedirectResponse
{
$post = $this->publisher->publish(
title: $request->validated('title'),
body: $request->validated('body'),
authorId: $request->user()->id,
);
return redirect()->route('posts.show', $post);
}
public function show(Post $post): View
{
// Route model binding handles the query — controller just passes data.
return view('posts.show', [
'post' => $post,
'comments' => $post->comments()->latest()->take(20)->get(),
]);
}
}
// app/Services/PostPublisher.php — business logic lives here
namespace App\Services;
use App\Events\PostPublished;
use App\Models\Post;
class PostPublisher
{
public function publish(string $title, string $body, int $authorId): Post
{
$post = Post::create([
'title' => $title,
'body' => $body,
'author_id' => $authorId,
'status' => 'published',
'published_at' => now(),
]);
// Side effects belong in the service, not the controller
event(new PostPublished($post));
return $post;
}
}
// app/Models/Post.php — Active Record model (Eloquent)
// This is the "M" in Laravel's MVC. It is NOT a pure domain model.
// It extends Model, meaning it has save(), delete(), find(), etc. built in.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
protected $fillable = ['title', 'body', 'author_id', 'status', 'published_at'];
protected $casts = [
'published_at' => 'datetime',
];
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
// Domain logic CAN live on the model for simple rules
public function isPublished(): bool
{
return $this->status === 'published' && $this->published_at?->isPast();
}
}Interview Q&A
Q: What is the difference between the original MVC pattern and what Laravel implements?
The original MVC from Smalltalk uses an active View that observes the Model directly — when Model state changes, the View re-renders without Controller involvement. Laravel uses a passive View (Blade templates) that receives data pushed from the Controller. There is no Model-to-View subscription. This is architecturally closer to MVP (Model-View-Presenter), but the industry calls it MVC. The key consequence is that Laravel's Controller owns the transformation and data-passing responsibility.
Q: What does "thin controller, fat model" mean, and is it good advice?
It means business logic should live on the model rather than the controller. It is better advice than "fat controller," but it is not the final answer. In Laravel, your Eloquent model is an Active Record that is already responsible for persistence — adding all business logic there violates the Single Responsibility Principle and makes testing harder. The more nuanced guidance for complex applications is "thin controller, thin model, fat service" — keep controllers as HTTP orchestrators, models as persistence representations, and put real business logic in dedicated Service or Domain classes.
Q: How do you decide whether to put logic in a controller, a service, or a model method?
Use the HTTP test: if the logic would need to run from a CLI command, a queue job, or a test without an HTTP request, it does not belong in the controller. Use the persistence test: if the logic does not involve database state or relationships, it may not belong on the model. Service classes are the right home for application-level business rules (publish a post, charge a customer, send a notification). Model methods are appropriate for rules that are intrinsically about that entity's own state (isPublished(), isOverdue()).