Skip to content

PHP Tutorial Practical PHP Development

Controllers and Views

Lesson 321 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

Mixing database queries, decisions and HTML in one file makes code hard to read and change. The MVC idea (Model, View, Controller) separates them:

PartJob
ModelThe data and its rules (a Product, a repository)
ViewThe HTML template. It only displays data
ControllerReceives the request, uses models, picks a view

The view: a template renderer

A view is a PHP file that prints data. A small function renders it with variables and returns the HTML as a string.

index.php
PHP
<?php
function render(string $template, array $data = []): string
{
    extract($data, EXTR_SKIP);

    ob_start();
    include $template;

    return (string) ob_get_clean();
}

function h(string $text): string
{
    return htmlspecialchars($text, ENT_QUOTES, "UTF-8");
}

file_put_contents(
    "product-list.php",
    "<h1><?= h(\$title) ?></h1>\n<ul>\n<?php foreach (\$products as \$p): ?>\n  <li><?= h(\$p['name']) ?></li>\n<?php endforeach; ?>\n</ul>"
);

echo render(__DIR__ . "/product-list.php", [
    "title" => "Our products",
    "products" => [["name" => "Pen"], ["name" => "Tom & Jerry book"]],
]);
Output
OUTPUT
<h1>Our products</h1>
<ul>
  <li>Pen</li>
  <li>Tom &amp; Jerry book</li>
</ul>

The template has no SQL, no $_GET, no business rules. It only loops and prints (with escaping).

The controller

A controller method is small: read the request, ask the model, render the view.

index.php
PHP
<?php
final class ProductController
{
    public function __construct(private ProductRepository $products) {}

    public function index(): array
    {
        $items = $this->products->all();

        return ["status" => 200, "view" => "product-list", "data" => ["title" => "Our products", "products" => $items]];
    }

    public function show(int $id): array
    {
        $product = $this->products->find($id);

        if ($product === null) {
            return ["status" => 404, "view" => "not-found", "data" => []];
        }

        return ["status" => 200, "view" => "product-show", "data" => ["product" => $product]];
    }
}

final class ProductRepository
{
    private array $rows = [1 => ["id" => 1, "name" => "Pen"], 2 => ["id" => 2, "name" => "Book"]];

    public function all(): array
    {
        return array_values($this->rows);
    }

    public function find(int $id): ?array
    {
        return $this->rows[$id] ?? null;
    }
}

$controller = new ProductController(new ProductRepository());

echo $controller->index()["status"] . " " . count($controller->index()["data"]["products"]) . " products\n";
echo $controller->show(2)["data"]["product"]["name"] . "\n";
echo $controller->show(99)["status"];
Output
OUTPUT
200 2 products
Book
404

The controller returns a description of the response (status, view name, data). It does not print. A small piece of code at the edge renders the view and sends it. That makes the controller easy to test: no HTML, no headers, no output.

What goes where

QuestionAnswer
Where does SQL go?Repository (model layer)
Where do business rules go?Services or the model
Where does $_GET/$_POST get read?The controller (or a request object)
Where does HTML go?The view
Where does header() / http_response_code() go?The edge of the app (front controller), not the views

Layouts

Pages share a header and footer. Render the page into a string, then pass it to a layout.

index.php
PHP
<?php
function renderLayout(string $title, string $content): string
{
    return "<!DOCTYPE html><html lang=\"en\"><head><title>" . htmlspecialchars($title) . "</title></head><body>" . $content . "</body></html>";
}

echo renderLayout("Home", "<h1>Welcome</h1>");
Output
OUTPUT
<!DOCTYPE html><html lang="en"><head><title>Home</title></head><body><h1>Welcome</h1></body></html>

Keep views dumb

  • Only display, loop and if.
  • Escape every value.
  • No queries, no calculations that belong in a service, no $_SESSION writes.

Template engines

Twig, Blade and Plates give templates a cleaner syntax and escape by default. For larger projects they are a good idea. Plain PHP templates work well for small ones, as long as you use the escape helper every time.

info
Why this matters in Laravel
Laravel controllers return views (return view("products.index", ["products" => $products]);) and the framework renders Blade templates. The structure matches this lesson.