Views & Templates

Melodic includes a lightweight template engine for rendering HTML pages. Templates are plain PHP files with a .phtml extension, and the engine provides layouts, sections, and a dynamic view bag — everything you need for server-rendered pages without the overhead of a full template language.

ViewEngine

The Melodic\View\ViewEngine class is the core of the template system. It renders templates, wraps them in layouts, and manages sections.

Constructor

use Melodic\View\ViewEngine;

$viewEngine = new ViewEngine(string $viewsPath);

The $viewsPath parameter is the base directory where all template files are located. All template names passed to render() are resolved relative to this path.

Rendering

$html = $viewEngine->render(
    string $template,       // Template name (without .phtml extension)
    array $data = [],       // Variables to extract into template scope
    ?string $layout = null, // Optional layout name (without .phtml extension)
): string;

The method resolves the template path as $viewsPath/$template.phtml, extracts the $data array into the template's variable scope, and captures the output. If a layout is specified, the rendered template output becomes the layout's body content.

Registering with the Container

Register the ViewEngine as a singleton in your application bootstrap so it is shared across all controllers:

use Melodic\View\ViewEngine;

$app->services(function ($container) use ($app) {
    $container->singleton(ViewEngine::class, function () use ($app) {
        return new ViewEngine($app->getBasePath() . '/views');
    });
});

Note: The ViewEngine is injected into MvcController via constructor injection. Once registered in the container, any controller extending MvcController will receive it automatically through auto-wiring.

Template Files

Templates are .phtml files — standard PHP mixed with HTML. They are located in your views directory and organized into subdirectories by feature or page.

views/
│── layouts/
│   └── main.phtml           ← Layout template
│── home/
│   │── index.phtml          ← Home page template
│   └── about.phtml          ← About page template
└── users/
    │── list.phtml           ← User list template
    └── show.phtml           ← User detail template

Data Variables

The $data array passed to render() is extracted into the template scope using PHP's extract(). Each array key becomes a local variable:

// In the controller
return $this->view('users/show', [
    'user' => $user,
    'postCount' => 42,
]);

// In views/users/show.phtml — $user and $postCount are available
<h1><?= htmlspecialchars($user->name) ?></h1>
<p>Posts: <?= $postCount ?></p>

Using PHP in Templates

Full PHP is available inside templates. Use loops, conditionals, and function calls as needed:

<ul>
    <?php foreach ($users as $user): ?>
        <li>
            <strong><?= htmlspecialchars($user->name) ?></strong>
            &mdash; <?= htmlspecialchars($user->email) ?>
        </li>
    <?php endforeach; ?>
</ul>

<?php if ($postCount > 0): ?>
    <p>Showing <?= $postCount ?> posts.</p>
<?php else: ?>
    <p>No posts yet.</p>
<?php endif; ?>

The $this Variable

Inside any template, $this refers to the ViewEngine instance. This gives you access to section methods (beginSection, endSection) and, in layouts, the renderBody() and renderSection() methods.

Important: Always escape user-provided data before outputting it. Use htmlspecialchars($var, ENT_QUOTES, 'UTF-8') to prevent XSS attacks. The short form htmlspecialchars($var) also works, but specifying ENT_QUOTES and UTF-8 is the safest practice.

Layouts

Layouts wrap your page templates with shared HTML structure — the doctype, head, navigation, footer, and other elements that repeat across pages. A layout is just another .phtml file that calls $this->renderBody() to output the page content.

Setting the Layout

In your controller, call setLayout() before returning the view. The argument is the path to the layout file relative to the views directory, without the .phtml extension:

class HomeController extends MvcController
{
    public function index(): Response
    {
        $this->setLayout('layouts/main');

        return $this->view('home/index', [
            'message' => 'Welcome to my app!',
        ]);
    }
}

Writing a Layout

A layout template provides the HTML shell. Use $this->renderBody() to insert the page template's output:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= htmlspecialchars($viewBag->title ?? 'My App') ?></title>
    <link rel="stylesheet" href="/css/app.css">
    <?= $this->renderSection('head') ?>
</head>
<body>
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
    </nav>

    <main>
        <?= $this->renderBody() ?>
    </main>

    <footer>
        <p>&copy; 2026 My App</p>
    </footer>

    <script src="/js/app.js"></script>
    <?= $this->renderSection('scripts') ?>
</body>
</html>

How Layout Rendering Works

  1. The ViewEngine renders the page template first, capturing its output
  2. The captured output is stored internally as the body content
  3. The layout template is then rendered, with access to the same data variables
  4. When the layout calls $this->renderBody(), the previously captured page output is inserted
  5. The final combined HTML is returned as the response body

Note: Melodic supports only one level of layout. Nested layouts (a layout wrapping another layout) are not supported. If you need shared structural elements across multiple layouts, extract them into partial templates and include them with include.

Sections

Sections allow page templates to inject content into specific slots defined in the layout. This is commonly used for page-specific CSS in the <head> or JavaScript before the closing </body> tag.

Defining a Section (in the Page Template)

Use $this->beginSection() and $this->endSection() to capture a block of content:

<!-- views/home/index.phtml -->
<h1><?= htmlspecialchars($message) ?></h1>
<p>This is the home page.</p>

<?php $this->beginSection('head') ?>
    <link rel="stylesheet" href="/css/home.css">
    <meta name="description" content="Home page">
<?php $this->endSection() ?>

<?php $this->beginSection('scripts') ?>
    <script src="/js/home.js"></script>
<?php $this->endSection() ?>

Rendering a Section (in the Layout)

Call $this->renderSection('name') in your layout to output the captured content:

<head>
    <!-- ... standard head content ... -->
    <?= $this->renderSection('head') ?>
</head>
<body>
    <?= $this->renderBody() ?>
    <?= $this->renderSection('scripts') ?>
</body>

If a section is not defined by the page template, renderSection() returns an empty string — no error, no output.

How Sections Work Internally

Sections use PHP's output buffering:

  1. beginSection('scripts') stores the section name and calls ob_start()
  2. Everything output between beginSection() and endSection() is captured by the buffer
  3. endSection() calls ob_get_clean() and stores the captured content keyed by the section name
  4. When the layout calls renderSection('scripts'), the stored content is returned

Common Section Patterns

Section NameTypical UsageLayout Placement
headPage-specific meta tags, stylesheets, inline CSSInside <head>, after common styles
scriptsPage-specific JavaScript files or inline scriptsBefore </body>, after common scripts

ViewBag

The Melodic\View\ViewBag is a dynamic property bag that lets you pass arbitrary key-value pairs from your controller to both the page template and the layout. It is automatically created by MvcController and included in the template data.

Setting Values (in the Controller)

class HomeController extends MvcController
{
    public function index(): Response
    {
        $this->setLayout('layouts/main');

        // Set viewBag properties — any name works
        $this->viewBag->title = 'Home Page';
        $this->viewBag->currentYear = date('Y');
        $this->viewBag->userContext = $this->getUserContext();

        return $this->view('home/index', [
            'message' => 'Welcome!',
        ]);
    }
}

Reading Values (in Templates and Layouts)

<!-- In the layout -->
<title><?= htmlspecialchars($viewBag->title ?? 'My App') ?></title>

<!-- In the page template -->
<footer>&copy; <?= $viewBag->currentYear ?></footer>

ViewBag API

FeatureDescription
$viewBag->key = 'value'Set a property (uses __set magic method)
$viewBag->keyGet a property (uses __get magic method). Returns null if not set.
isset($viewBag->key)Check if a property exists (uses __isset magic method)
$viewBag->toArray()Convert all properties to an associative array

Tip: Accessing an undefined property on the ViewBag returns null rather than throwing an error. Use the null coalescing operator for defaults: $viewBag->title ?? 'Untitled'.

MvcController View Methods

The Melodic\Controller\MvcController base class provides the methods that tie the template system together. Any controller that renders HTML views should extend this class.

MethodDescription
view(string $template, array $data = []): Response Renders the named template (relative to the views directory, without .phtml), wraps it in the current layout if set, and returns a 200 HTML Response. The $viewBag is automatically included in the data array.
setLayout(string $layout): void Sets the layout template to wrap page output. Pass the path relative to the views directory, without .phtml (e.g., 'layouts/main').
viewBag(): ViewBag Returns the ViewBag instance. You can also access it directly via the $this->viewBag property.
getUserContext(): ?UserContextInterface Returns the UserContext from the request (set by authentication middleware). Returns null if no authentication middleware ran.

The view() method automatically injects the ViewBag into the template data as the $viewBag variable. This means the ViewBag is accessible in both the page template and the layout without any extra work.

Complete Example

Here is a full example showing a controller, layout, and page template working together with sections and the ViewBag.

Controller

<?php

declare(strict_types=1);

namespace App\Controllers;

use Melodic\Controller\MvcController;
use Melodic\Http\Response;

class HomeController extends MvcController
{
    public function index(): Response
    {
        $this->setLayout('layouts/main');

        $this->viewBag->title = 'Home';
        $this->viewBag->userContext = $this->getUserContext();

        return $this->view('home/index', [
            'heading' => 'Welcome to My App',
            'features' => ['Fast', 'Secure', 'Simple'],
        ]);
    }
}

Layout — views/layouts/main.phtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= htmlspecialchars($viewBag->title ?? 'My App') ?></title>
    <link rel="stylesheet" href="/css/app.css">
    <?= $this->renderSection('head') ?>
</head>
<body>
    <header>
        <nav>
            <a href="/">Home</a>
            <a href="/about">About</a>
            <?php $uc = $viewBag->userContext; ?>
            <?php if ($uc && $uc->isAuthenticated()): ?>
                <span><?= htmlspecialchars($uc->getUsername()) ?></span>
                <a href="/auth/logout">Logout</a>
            <?php else: ?>
                <a href="/auth/login">Sign In</a>
            <?php endif; ?>
        </nav>
    </header>

    <main>
        <?= $this->renderBody() ?>
    </main>

    <footer>
        <p>&copy; 2026 My App</p>
    </footer>

    <script src="/js/app.js"></script>
    <?= $this->renderSection('scripts') ?>
</body>
</html>

Page Template — views/home/index.phtml

<h1><?= htmlspecialchars($heading) ?></h1>

<ul>
    <?php foreach ($features as $feature): ?>
        <li><?= htmlspecialchars($feature) ?></li>
    <?php endforeach; ?>
</ul>

<?php $this->beginSection('head') ?>
    <meta name="description" content="Welcome to My App">
    <link rel="stylesheet" href="/css/home.css">
<?php $this->endSection() ?>

<?php $this->beginSection('scripts') ?>
    <script src="/js/home.js"></script>
    <script>
        console.log('Home page loaded');
    </script>
<?php $this->endSection() ?>

Rendered Output

When a request hits GET /, the controller renders home/index.phtml inside layouts/main.phtml. The final HTML sent to the browser combines them:

  • The <head> includes both the layout's common stylesheet and the page's section content (meta tag and home.css)
  • The <main> contains the rendered page body (heading and feature list)
  • The bottom of <body> includes both the common app.js and the page's scripts section

Cached Rendering

The ViewEngine supports cached rendering via the renderCached() method. When a CacheInterface instance is available, rendered output is stored in the cache and reused on subsequent requests, avoiding redundant template processing.

Method Signature

$html = $viewEngine->renderCached(
    string $template,       // Template name (without .phtml extension)
    array $data = [],       // Variables to extract into template scope
    ?string $layout = null, // Optional layout name (without .phtml extension)
    int $ttl = 3600,        // Cache time-to-live in seconds (default: 1 hour)
): string;

How It Works

  1. If no cache is configured on the ViewEngine, the method falls back to a normal render() call with no caching.
  2. A cache key is generated from the template name, layout name, and a hash of the serialized data array.
  3. If a cached entry exists for that key, the cached HTML is returned immediately without rendering.
  4. If no cached entry exists, the template is rendered normally, stored in the cache with the given TTL, and then returned.

Usage

Use renderCached() directly on the ViewEngine for templates that are expensive to render but do not change frequently, such as documentation pages, marketing content, or reports:

// Cache the rendered output for 30 minutes
$html = $viewEngine->renderCached('docs/getting-started', $data, 'layouts/docs', ttl: 1800);

Important: Cached rendering stores the fully rendered HTML, including any user-specific data passed in the $data array. Do not use renderCached() for pages that include personalized content (e.g., the current user's name) unless that data is part of the cache key. Since the cache key includes a hash of the data array, different data produces different cache entries.

Tip: For pages where only a portion of the content is dynamic, render the static portions with renderCached() and inject the dynamic portions separately. This gives you the performance benefit of caching while keeping user-specific content fresh.

Best Practices

Always Escape Output

Any data that comes from user input, a database, or an external source must be escaped before output. Use htmlspecialchars() consistently:

<!-- Safe -->
<p><?= htmlspecialchars($user->bio, ENT_QUOTES, 'UTF-8') ?></p>

<!-- Dangerous — never do this with user data -->
<p><?= $user->bio ?></p>

Use ViewBag for Layout-Level Data

Data that the layout needs — page titles, user context for navigation, breadcrumbs — should go on the ViewBag so it is accessible in both the layout and the page template:

$this->viewBag->title = 'User Profile';
$this->viewBag->userContext = $this->getUserContext();
$this->viewBag->breadcrumbs = ['Home' => '/', 'Profile' => null];

Use Template Data for Page-Specific Data

Data that only the page template needs should go in the $data array passed to view(). This keeps the separation clear and avoids polluting the ViewBag with page-specific concerns:

return $this->view('users/show', [
    'user' => $user,
    'recentPosts' => $posts,
]);

Keep Templates Thin

Templates should focus on rendering. Business logic, data fetching, and complex computations belong in your controllers and services. A template should receive prepared data and output HTML — nothing more.

<!-- Good: template receives prepared data -->
<p>Total: $<?= number_format($orderTotal, 2) ?></p>

<!-- Avoid: complex logic in templates -->
<?php
$total = 0;
foreach ($items as $item) {
    $total += $item->price * $item->quantity;
    if ($item->hasDiscount()) {
        $total -= $item->getDiscountAmount();
    }
}
?>
<p>Total: $<?= number_format($total, 2) ?></p>

Use Sections for Page-Specific Assets

Keep common CSS and JavaScript in the layout. Use sections only for assets that are specific to a single page. This keeps page load times fast and avoids loading unnecessary resources:

<!-- In a page that uses a map library -->
<?php $this->beginSection('head') ?>
    <link rel="stylesheet" href="/css/leaflet.css">
<?php $this->endSection() ?>

<?php $this->beginSection('scripts') ?>
    <script src="/js/leaflet.js"></script>
    <script src="/js/map-page.js"></script>
<?php $this->endSection() ?>