MVC Pattern
debt(d7/e5/b7/t7)
Closest to 'only careful code review or runtime testing' (d7). The term's detection_hints.tools is not specified. Fat controllers and logic-in-views are not caught by compilers or standard linters — the code runs fine syntactically. A specialist static analysis tool could flag method length or complexity, but no standard tool enforces MVC layer boundaries. Typically only careful code review surfaces the structural violation.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix says to move logic from controllers into Service classes or model methods — this is not a one-line patch. A fat controller with embedded validation, DB queries, email-sending, and formatting requires extracting multiple concerns into new classes, updating tests, and adjusting dependency injection — spanning several files even within one feature.
Closest to 'strong gravitational pull' (e7). MVC applies to both web and cli contexts across the entire PHP application. Controllers are request entry-points for every feature; if fat-controller patterns are established early, every new feature inherits the pattern. Correcting the architecture later requires touching almost every controller and creating a new service layer — the structural choice shapes how every developer writes every action going forward.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field explicitly states that developers believe controllers should contain business logic because they handle requests — this is the 'obvious' intuition for most beginners and even intermediate developers. The correct behaviour (thin orchestrator) directly contradicts the natural mental model of 'the controller controls everything,' and the mistake is pervasive enough that the term's why_it_matters calls it 'the most common structural mistake in PHP applications.'
Also Known As
TL;DR
Explanation
MVC separates an application into three layers: Model — represents data and business rules, queries the database, validates constraints, and enforces domain logic. View — renders the output (HTML, JSON, XML) from data provided by the controller. Controller — handles incoming requests, calls the appropriate model methods, and passes results to the view. The flow: HTTP request → Router → Controller → Model → Controller → View → HTTP response. PHP frameworks implement MVC with variations: Laravel uses Eloquent Active Record models, Blade view templates, and thin controllers; Symfony uses Doctrine Data Mapper models, Twig templates, and richer controllers. Common MVC violations: fat controllers (business logic in controllers), smart views (queries or logic in templates), and dumb models (models as mere database wrappers). The fat model / thin controller principle pushes business logic into models or dedicated service classes.
Common Misconception
Why It Matters
Common Mistakes
- Fat controllers — putting database queries, validation logic, and email sending directly in controller methods.
- Logic in views — performing database queries or complex calculations in Blade/Twig templates.
- Models as data bags — models that only have fillable/casts with no business methods are not using MVC correctly; models should enforce their own invariants.
- One controller per table — controllers should be organised around user actions and HTTP resources, not database tables.
Code Examples
// Fat controller — untestable, all concerns mixed
class OrderController {
public function store(Request $r) {
if (!$r->user()->can('create-orders')) abort(403);
$total = array_sum(array_column($r->items, 'price'));
if ($total > 10000) throw new Exception('Order too large');
$order = Order::create(['user_id' => $r->user()->id, 'total' => $total]);
foreach ($r->items as $item) OrderItem::create([...]);
Mail::to($r->user())->send(new OrderConfirmed($order));
Slack::notify('#orders', 'New order: ' . $order->id);
return response()->json($order, 201);
}
}
// Thin controller — delegates to service
class OrderController {
public function store(Request $r, OrderService $service) {
$this->authorize('create', Order::class);
$order = $service->createOrder($r->user(), $r->validated());
return new OrderResource($order);
}
}
// Service holds business logic — independently testable
class OrderService {
public function createOrder(User $user, array $data): Order {
$order = Order::createFromItems($user, $data['items']);
OrderConfirmation::dispatch($order); // queued job
return $order;
}
}