iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

MVC / MVVM

Model-View-Controller separates data, presentation, and orchestration. Models own state and rules; views render; controllers route requests and orchestrate. Rails canonised it; Spring, ASP.NET, Laravel, and Django all run with it. Done well, you can swap views or models in isolation; done poorly, the controller becomes a god-class.

A worked MVC slice

EXAMPLE
<?php
// ============================================================
// Model — owns persistence + business rules
// ============================================================
final class Order {
    public function __construct(
        public string $id,
        public string $customer,
        public int $totalCents,
        public string $status = 'new',
        public ?DateTimeImmutable $paidAt = null,
    ) {}

    // Business rule lives in the model, not the controller
    public function pay(DateTimeImmutable $at): void {
        if ($this->status !== 'new') {
            throw new DomainException('cannot pay from ' . $this->status);
        }
        $this->status = 'paid';
        $this->paidAt = $at;
    }

    public function totalAud(): float {
        return $this->totalCents / 100;
    }
}

interface OrderRepository {
    public function findById(string $id): ?Order;
    public function save(Order $o): void;
}

// ============================================================
// Controller — orchestrates the request, returns a view
// ============================================================
final class OrdersController {
    public function __construct(
        private OrderRepository $repo,
        private Clock $clock,
    ) {}

    // GET /orders/{id}
    public function show(Request $req): Response {
        $order = $this->repo->findById($req->route('id'));
        if (!$order) return Response::notFound();
        return view('orders.show', ['order' => $order]);
    }

    // POST /orders/{id}/pay
    public function pay(Request $req): Response {
        $order = $this->repo->findById($req->route('id'));
        if (!$order) return Response::notFound();

        try {
            $order->pay($this->clock->now());
            $this->repo->save($order);
        } catch (DomainException $e) {
            return Response::back()->withError($e->getMessage());
        }
        return Response::redirect('/orders/' . $order->id)->with('flash', 'paid');
    }
}

// ============================================================
// View — rendering only, no business logic
// ============================================================
// resources/views/orders/show.blade.php
?>
<h1>Order {{ $order->id }}</h1>
<dl>
  <dt>Customer</dt><dd>{{ $order->customer }}</dd>
  <dt>Total</dt>   <dd>{{ number_format($order->totalAud(), 2) }} AUD</dd>
  <dt>Status</dt>  <dd>{{ $order->status }}</dd>
</dl>

@if ($order->status === 'new')
  <form method='POST' action='/orders/{{ $order->id }}/pay'>
    @csrf
    <button>Mark paid</button>
  </form>
@endif

<?php
// ============================================================
// Anti-patterns to avoid
// ============================================================
// 1) Business logic in the view
//    if ($order->totalCents > 50000 && $user->tier === 'gold') ...
//    -> push into the model or a domain service

// 2) Business logic in the controller
//    if ($order->status === 'paid') { $invoice->status = 'void'; ... }
//    -> push into the model ($order->cancel()) or a service object

// 3) Active Record god model
//    Order class with 200 methods including sending email and posting to Slack
//    -> extract a service (PayOrder, ShipOrder) when responsibilities grow

// 4) Skinny model, fat controller
//    Controller with 200 lines of orchestration logic
//    -> push behaviour back into the model OR a service object

// ============================================================
// MVC vs the cousins
// ============================================================
// MVP (Model-View-Presenter): the View is dumb; the Presenter handles UI logic
// MVVM (Model-View-ViewModel): the ViewModel exposes observable state; popular
//   in WPF, Vue (computed/refs), SwiftUI (Observable)
// Hexagonal / Clean: MVC + explicit ports for storage, queues, services
//
// Pick MVC for server-rendered web apps; MVVM for desktop / declarative UI;
// Clean Architecture when the team is large enough to enforce the layering.

interface Clock { public function now(): DateTimeImmutable; }
class Request {
    public function route(string $k): string { return 'o1'; }
    public function user(): mixed { return null; }
}
class Response {
    public static function notFound(): self { return new self(); }
    public static function redirect(string $u): self { return new self(); }
    public static function back(): self { return new self(); }
    public function withError(string $m): self { return $this; }
    public function with(string $k, mixed $v): self { return $this; }
}
function view(string $name, array $data): Response { return new Response(); }

Why it matters

When a controller grows past about 10 lines per action, pull the work out into a model method or a service object. Skinny controllers + meaningful models + thin views is the shape that scales: tests stay focused, code review stays cheap, and refactors stay local.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Model       — state + rules.
// View        — display + input.
// Controller  — orchestrates: turns input into model updates, picks the next view.
// MVVM swaps Controller for a ViewModel that the view binds to.
Try it Yourself »

Discussion

Loading…