Views & Layouts

Last updated: 04/17/2026 · Written by Agent0

Views & Layouts

Views are plain PHP files that render HTML. They live in resources/views/ and are organized into subfolders by feature. There's no custom templating language — just PHP, with a set of global helpers to make common tasks cleaner.


View Files & Folder Structure

Views are referenced in controllers using dot notation. Each dot maps to a folder separator.

// Dot notation        →   File path
'app.articles.index'  →   resources/views/app/articles/index.php
'app.dashboard'       →   resources/views/app/dashboard.php
'home.index'          →   resources/views/home/index.php

The default folder structure groups views by context:

resources/views/
├── app/              ← Authenticated app views
│   ├── dashboard.php
│   ├── profile.php
│   └── articles/
│       ├── index.php
│       └── form.php
├── auth/             ← Login, register, reset, etc.
├── errors/           ← 403, 404, 500 error pages
├── home/             ← Public-facing pages
└── layouts/          ← Layout wrappers
    ├── app.php       ← Sidebar + top nav layout
    ├── public.php    ← Public page layout
    ├── base.php      ← Root HTML shell (head, scripts, flash messages)
    └── partials/
        ├── top_nav.php
        └── side_nav.php

Layouts

Every view is wrapped in a layout. The layout is the third argument passed to $this->render() in your controller.

layouts.app — Authenticated pages

Use this for all pages inside the logged-in area. Includes the top navigation bar and the sidebar.

return $this->render('app.articles.index', [
    'title'    => 'Articles',
    'articles' => $articles,
], 'layouts.app');

layouts.public — Public pages

Use this for pages that don't require login — landing pages, marketing pages, etc. Includes the top navigation bar only, no sidebar.

return $this->render('home.index', [
    'title' => 'Welcome',
], 'layouts.public');

Both layouts inject your view's rendered HTML through the $content variable, which is handled automatically — you don't manage this yourself.

The outer shell — layouts/base.php — wraps everything. It contains the <html>, <head>, Tailwind CDN, Font Awesome, the theme system, flash message rendering, and TinyMCE. You rarely need to edit this directly.


Passing Data to Views

The data array passed to $this->render() is extracted into variables that are available directly in the view. A key of 'articles' becomes $articles in the view.

// Controller
return $this->render('app.articles.index', [
    'title'    => 'Articles',
    'articles' => $articles,
    'total'    => $total,
], 'layouts.app');

// In the view — use them directly
<h1><?= e($title) ?></h1>
<p><?= $total ?> articles found.</p>

The $title variable is also used by layouts/base.php to populate the <title> tag in the browser tab automatically.


Escaping Output

Always use the e() helper when rendering user-supplied data in a view. It escapes HTML special characters to prevent XSS attacks.

<?= e($article->title) ?>
<?= e($user->name) ?>
<?= e(auth('email')) ?>

Skip e() only when you intentionally want to render raw HTML — for example, content from TinyMCE that was already sanitized before saving.


A Typical View File

Views contain only the page-specific content — no <html>, no <head>, no layout wrapper. The layout takes care of all of that.

<div class="space-y-6">

    <!-- Page Header -->
    <div class="flex items-center justify-between">
        <div>
            <h1 class="text-2xl font-semibold text-gray-800 dark:text-gray-100">
                Articles
            </h1>
            <p class="text-sm text-gray-500 dark:text-gray-400">
                Manage all articles.
            </p>
        </div>
        <a href="/articles/form" class="btn btn-primary">+ New Article</a>
    </div>

    <!-- Breadcrumbs -->
    <nav class="text-sm text-gray-500 dark:text-gray-400">
        <a href="/dashboard" class="hover:underline">Dashboard</a>
        <span class="mx-1">/</span>
        <span class="text-gray-700 dark:text-gray-200">Articles</span>
    </nav>

    <!-- Card -->
    <div class="card">
        <?php if (empty($articles)): ?>
            <p class="text-sm text-muted">No articles found.</p>
        <?php else: ?>
            <?php foreach ($articles as $article): ?>
                <p><?= e($article->title) ?></p>
            <?php endforeach; ?>
        <?php endif; ?>
    </div>

</div>

Form Helpers in Views

StackCTL provides a set of helpers specifically for working with forms after a failed validation redirect.

old() — Repopulate field values

Retrieves the previously submitted value for a field so users don't have to retype everything after a validation error.

<input type="text" name="title" value="<?= e(old('title')) ?>">

For edit forms, fall back to the existing record value when no old input is present:

<input type="text" name="title" value="<?= e($article ? $article->title : old('title')) ?>">

errors() and error() — Display validation errors

Call errors() once at the top of the form to retrieve all errors, then use error() per field to render the message.

<?php $errors = errors(); ?>

<div>
    <label class="label">Title</label>
    <input type="text" name="title"
           value="<?= e(old('title')) ?>"
           class="input <?= has_error($errors, 'title') ?>">
    <?= error($errors, 'title') ?>
</div>
  • errors() — Returns the full array of validation errors from the flash store
  • error($errors, 'field') — Renders the error message HTML for a field, or empty string if none
  • has_error($errors, 'field') — Returns 'input-error' CSS class if the field has an error, otherwise empty string

csrf_field() — CSRF protection

Include this inside every POST form. StackCTL verifies the token automatically on every POST request — a missing or invalid token aborts with a 419 error.

<form method="POST" action="/articles/save">
    <?= csrf_field() ?>
    ...
</form>

Auth Helpers in Views

Use these anywhere in a view to check the current user's state or conditionally show content.

<!-- Show content only to logged-in users -->
<?php if (is_auth()): ?>
    <p>Welcome, <?= e(auth('name')) ?></p>
<?php endif; ?>

<!-- Show content only to admins -->
<?php if (has_role('admin')): ?>
    <a href="/admin">Admin Panel</a>
<?php endif; ?>

<!-- Show an action button based on a permission -->
<?php if (can('edit', 'article')): ?>
    <a href="/articles/form/<?= $article->id ?>" class="btn btn-secondary">Edit</a>
<?php endif; ?>

Active Navigation Links

Use the is_active() helper in your sidebar or navigation to highlight the current page link.

<a href="/articles" class="nav-link <?= is_active('/articles') ?>">
    Articles
</a>

<!-- Partial match — highlights for /articles and any sub-path -->
<a href="/articles" class="nav-link <?= is_active('/articles', false) ?>">
    Articles
</a>

Pass false as the second argument for a prefix match instead of an exact match — useful for sections with sub-pages.

Was this helpful?