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 storeerror($errors, 'field')— Renders the error message HTML for a field, or empty string if nonehas_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.