MVC Pattern — Model, View, Controller
MVC (Model-View-Controller) separates concerns into three layers: the Model handles data and business logic, the View renders output, and the Controller orchestrates request handling. PHP MVC is the foundation of every major framework (Laravel, Symfony, Slim). Understanding vanilla MVC first makes learning any framework significantly easier.
MVC — Full Layer Separation
<?php
declare(strict_types=1);
// ── Controller — handles request, calls model, passes to view ─
namespace App\Http\Controller;
use App\Blog\Service\PostService;
use App\Http\{Request, Response};
class PostController {
public function __construct(
private readonly PostService $postService,
) {}
// GET /posts
public function index(Request $req): Response {
$page = max(1, (int) $req->query("page", 1));
$search = trim($req->query("search", ""));
$paginated = $this->postService->listPublished($page, perPage: 10, search: $search);
return Response::view("posts/index", [
"posts" => $paginated["items"],
"meta" => $paginated,
"searchTerm" => $search,
]);
}
// GET /posts/{slug}
public function show(Request $req): Response {
$slug = $req->param("slug");
$post = $this->postService->findBySlug($slug);
if ($post === null) return Response::notFound();
return Response::view("posts/show", ["post" => $post]);
}
// GET /posts/create — show form
public function create(Request $req): Response {
Auth::requireLogin();
return Response::view("posts/create");
}
// POST /posts — save new post
public function store(Request $req): Response {
Auth::requireLogin();
CsrfToken::validate($req->body("_csrf_token") ?? "");
$v = new Validator();
$v->required("title", $req->body("title"), "Title")
->minLength("title", $req->body("title", ""), 3, "Title")
->required("body", $req->body("body"), "Content");
if ($v->fails()) {
return Response::view("posts/create", [
"errors" => $v->errors(),
"old" => $req->body(),
], 422);
}
$post = $this->postService->create([
"title" => trim($req->body("title")),
"body" => trim($req->body("body")),
"author_id" => Auth::userId(),
]);
return Response::redirect("/posts/{$post->slug}");
}
}
// ── View — presentation layer (PHP template) ──────────────────
// views/posts/index.php
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blog Posts</title>
</head>
<body>
<main>
<h1>Blog Posts</h1>
<?php if (!empty($searchTerm)): ?>
<p>Showing results for: <strong><?= e($searchTerm) ?></strong></p>
<?php endif; ?>
<?php foreach ($posts as $post): ?>
<article>
<h2><a href="/posts/<?= e($post->slug) ?>"><?= e($post->title) ?></a></h2>
<p>By <?= e((string) $post->author) ?> — <?= e($post->createdAt->format("M j, Y")) ?></p>
</article>
<?php endforeach; ?>
<?php if ($meta["last_page"] > 1): ?>
<nav>
<?php for ($i = 1; $i <= $meta["last_page"]; $i++): ?>
<a href="/posts?page=<?= $i ?>" <?= $i === $meta["page"] ? 'aria-current="page"' : '' ?>>
<?= $i ?>
</a>
<?php endfor; ?>
</nav>
<?php endif; ?>
</main>
</body>
</html>
<?php
// ── Model — PostService is the Model layer (business logic) ──
// (Already built in Modules 5-9 as PostService + PostRepository)
// ── Response class — Controller returns this ──────────────────
class Response {
private function __construct(
private readonly string $type, // "view" | "redirect" | "json"
private readonly mixed $data,
private readonly int $status,
) {}
public static function view(string $template, array $vars = [], int $status = 200): static {
return new static("view", ["template" => $template, "vars" => $vars], $status);
}
public static function redirect(string $url, int $status = 302): static {
return new static("redirect", $url, $status);
}
public static function notFound(): static { return static::view("errors/404", [], 404); }
public function send(): void {
http_response_code($this->status);
match ($this->type) {
"redirect" => (function() { header("Location: $this->data"); exit; })(),
"view" => (function() {
extract($this->data["vars"]);
include __DIR__ . "/../../views/{$this->data['template']}.php";
})(),
};
}
}Quick Quiz
Tip
Tip
Practice MVC Pattern Model View Controller in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
PHP processes each request through the server-side engine
Practice Task
Note
Practice Task — (1) Write a working example of MVC Pattern Model View Controller from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Common Mistake
Warning
A common mistake with MVC Pattern Model View Controller is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready php code.