mini\Template
namespace
Template - PHP Template Rendering
Philosophy
Mini's template system is pure PHP with optional inheritance:
- Native PHP syntax - No special template language to learn
- Inheritance support - Multi-level layouts via
$this->extend()and$this->block() - ViewStart convention - Automatic
_viewstart.phpinclusion (like ASP.NET Core) - Path registry - Automatic template discovery in
_views/directory - Zero configuration - Works out of the box with sensible defaults
- Flexible - Use inheritance, or just plain PHP files
Setup
Templates are stored in _views/ by default:
project/
├── _views/
│ ├── _viewstart.php # Auto-included, sets $layout default
│ ├── _layout.php # Main layout
│ ├── home.php # Homepage template
│ ├── settings.php # Settings page
│ ├── admin/
│ │ ├── _viewstart.php # Admin-specific setup (optional)
│ │ └── dashboard.php
│ └── partials/
│ └── user-card.php # Reusable component
No configuration needed - Mini automatically finds templates in _views/.
Basic Usage
Simple Template (No Inheritance)
// _routes/settings.php
echo render('settings.php', ['user' => $currentUser]);
// _views/settings.php
<h1>Settings for <?= htmlspecialchars($user['name']) ?></h1>
<form>
<input name="email" value="<?= htmlspecialchars($user['email']) ?>">
<button>Save</button>
</form>
Template with Layout Inheritance
Child template:
// _views/home.php
<?php $this->extend(); ?> <!-- Uses $layout from _viewstart.php -->
<?php $this->block('title', 'Home Page'); ?>
<?php $this->block('content'); ?>
<h1>Welcome!</h1>
<p>This is the home page content.</p>
<?php $this->end(); ?>
Parent layout:
// _views/_layout.php
<!DOCTYPE html>
<html>
<head>
<title><?php $this->show('title', 'My App'); ?></title>
</head>
<body>
<header>
<nav>...</nav>
</header>
<main>
<?php $this->show('content'); ?>
</main>
<footer>...</footer>
</body>
</html>
Render:
// _routes/index.php
echo render('home.php');
Template Helpers
Templates have access to four methods on $this:
$this->extend(?string $layout = null)
Marks the current template as extending a parent layout:
<?php $this->extend(); ?> // Use default $layout from _viewstart.php
<?php $this->extend('_layout.php'); ?> // Explicit layout
$this->block(string $name, ?string $value = null)
Defines a block (two modes):
Inline mode:
<?php $this->block('title', 'My Page Title'); ?>
Buffered mode:
<?php $this->block('content'); ?>
<p>Multiple lines of content...</p>
<p>...captured until $this->end() is called.</p>
<?php $this->end(); ?>
$this->end()
Ends a buffered block started with $this->block():
<?php $this->block('sidebar'); ?>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<?php $this->end(); ?>
$this->show(string $name, string $default = '')
Outputs a block in parent templates:
<title><?php $this->show('title', 'Default Title'); ?></title>
<div class="content"><?php $this->show('content'); ?></div>
ViewStart Files
Mini automatically includes _viewstart.php files before rendering templates, similar to ASP.NET Core's _ViewStart.cshtml. This is useful for:
- Setting a default
$layoutfor templates - Defining variables available to all templates in a directory tree
- Running setup code before templates render
How It Works
When rendering admin/users/list.php, Mini includes _viewstart.php files in order:
_views/_viewstart.php(root - runs first)_views/admin/_viewstart.php(if exists)_views/admin/users/_viewstart.php(if exists - runs last)
Later files can override variables set by earlier files.
Default Layout
The root _viewstart.php typically sets the default layout:
// _views/_viewstart.php
<?php
$layout = '_layout.php';
Templates can then use $this->extend() without arguments:
// _views/page.php
<?php $this->extend(); ?> // Uses '_layout.php' from _viewstart.php
<?php $this->block('content'); ?>
<p>Page content</p>
<?php $this->end(); ?>
Section-Specific Setup
Create _viewstart.php in subdirectories for section-specific configuration:
// _views/admin/_viewstart.php
<?php
$layout = '_admin-layout.php'; // Override default layout for admin
$adminSection = true; // Variable available to all admin templates
// _views/admin/dashboard.php
<?php $this->extend(); ?> // Uses '_admin-layout.php'
<?php $this->block('content'); ?>
<h1>Admin Dashboard</h1>
<?php if ($adminSection): ?>
<p>You are in the admin section.</p>
<?php endif; ?>
<?php $this->end(); ?>
Event Hooks
Use _viewstart.php to fire events for extensibility:
// _views/_viewstart.php
<?php
$layout = '_layout.php';
// Allow plugins to inject variables or modify rendering
mini\emit('view.start', [
'template' => $__template ?? null,
]);
Multi-Level Inheritance
You can extend layouts that themselves extend other layouts:
// _views/two-column.php
<?php $this->extend('_layout.php'); ?>
<?php $this->block('content'); ?>
<div class="row">
<div class="col-main"><?php $this->show('main'); ?></div>
<div class="col-sidebar"><?php $this->show('sidebar'); ?></div>
</div>
<?php $this->end(); ?>
// _views/dashboard.php
<?php $this->extend('two-column.php'); ?>
<?php $this->block('title', 'Dashboard'); ?>
<?php $this->block('main'); ?>
<h1>Dashboard</h1>
<p>Main content here</p>
<?php $this->end(); ?>
<?php $this->block('sidebar'); ?>
<ul>
<li>Sidebar item</li>
</ul>
<?php $this->end(); ?>
Including Partials
Use render() to include reusable components:
// _views/users.php
<div class="users">
<?php foreach ($users as $user): ?>
<?= mini\render('partials/user-card.php', ['user' => $user]) ?>
<?php endforeach; ?>
</div>
// _views/partials/user-card.php
<div class="user-card">
<img src="<?= htmlspecialchars($user['avatar']) ?>">
<h3><?= htmlspecialchars($user['name']) ?></h3>
<p><?= htmlspecialchars($user['email']) ?></p>
</div>
Advanced Examples
Dynamic Layouts
// Choose layout based on user role
$layout = $user['role'] === 'admin' ? '_admin-layout.php' : '_layout.php';
// _views/page.php
<?php $this->extend($layout); ?>
<?php $this->block('content'); ?>
<p>Content here</p>
<?php $this->end(); ?>
Conditional Blocks
// _views/_layout.php
<!DOCTYPE html>
<html>
<head>
<title><?php $this->show('title', 'My App'); ?></title>
<?php if (isset($this->blocks['meta'])): ?>
<?php $this->show('meta'); ?>
<?php endif; ?>
</head>
<body>
<?php $this->show('content'); ?>
</body>
</html>
Block Default Content
// _views/_layout.php
<aside class="sidebar">
<?php $this->show('sidebar', '<p>Default sidebar content</p>'); ?>
</aside>
Escaping Output
Always escape user-provided data. Use the mini\h() helper for concise escaping:
<?= mini\h($user['name']) ?>
<!-- Equivalent to: -->
<?= htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8') ?>
<!-- For HTML attributes -->
<input value="<?= mini\h($user['email']) ?>">
<!-- For JSON in inline scripts -->
<script>
const user = <?= json_encode($user, JSON_HEX_TAG | JSON_HEX_AMP) ?>;
</script>
Configuration
Custom Views Directory
Set via environment variable:
MINI_VIEWS_ROOT=/path/to/custom/views
Or configure programmatically:
// Before render() is first called
Mini::$mini->paths->views = new PathsRegistry('/custom/views');
Multiple View Paths
Add additional search paths:
// Search in theme directory, then fallback to default views
Mini::$mini->paths->views->addPath('/path/to/theme/views');
Templates are found using first-match from the path registry.
Resolution Order
Templates are resolved using PathRegistry:
- Application:
_views/(orMINI_VIEWS_ROOTenv var) - always first (primary path) - Third-party packages: Their
_views/directories (most recently added first) - Framework:
vendor/fubber/mini/_views/- always last
Composer Package Integration
Third-party Composer packages can provide templates by calling addPath() during bootstrap. Since Composer loads autoload.files in dependency graph order (framework → packages → application), and addPath() inserts new paths before earlier fallbacks, the resolution order naturally allows packages to override framework templates.
Package composer.json:
{
"name": "vendor/my-package",
"autoload": {
"files": ["src/bootstrap.php"]
}
}
Package bootstrap.php:
use mini\Mini;
Mini::$mini->paths->views->addPath(__DIR__ . '/../_views');
Package structure:
my-package/
├── _views/
│ └── my-package/
│ ├── widget.php
│ └── partials/
└── src/
└── bootstrap.php
Templates become available as my-package/widget.php etc.
To override a package's template, place your version at the same relative path in your application's _views/ directory.
Custom Renderer
Replace the default renderer:
// _config/mini/Template/RendererInterface.php
return new App\CustomRenderer();
How It Works
- Template Discovery: Uses path registry to locate template files
- ViewStart: Includes
_viewstart.phpfiles from root to template directory (stacked) - Rendering: PHP file is included with extracted variables
- Block Capture:
$this->block()captures content via output buffering - Inheritance: If
$this->extend()was called, re-render parent with blocks - Output: Final rendered content returned as string
The renderer handles multi-level inheritance by recursively rendering parent templates, passing captured blocks upward through the __blocks variable. ViewStart files only run for the initial template, not for parent layouts.
Template Variables
All variables passed to render() are available directly:
echo render('user.php', ['name' => 'John', 'email' => 'john@example.com']);
// _views/user.php - $name and $email are available
<h1><?= htmlspecialchars($name) ?></h1>
<p><?= htmlspecialchars($email) ?></p>
Error Handling
If a template is not found, an exception is thrown:
try {
echo render('missing.php');
} catch (\Exception $e) {
// Template not found: missing.php (searched in: /path/to/_views)
}
Rendering errors are caught and returned as strings for easier debugging during development.
Best Practices
- Always escape output - Use
htmlspecialchars()for user data - Keep logic minimal - Templates should be mostly HTML
- Use partials for reuse - Don't repeat card/list markup
- Consistent naming - Use kebab-case for template filenames
- Organize by feature - Group related templates in subdirectories
Examples Repository
Blog Post Layout
// _views/blog/post.php
<?php $this->extend(); ?>
<?php $this->block('title', htmlspecialchars($post['title'])); ?>
<?php $this->block('content'); ?>
<article>
<h1><?= htmlspecialchars($post['title']) ?></h1>
<time><?= $post['published_at'] ?></time>
<div class="content">
<?= $post['body'] ?> <!-- Already sanitized -->
</div>
</article>
<?php $this->end(); ?>
Admin Dashboard
// _views/admin/dashboard.php
<?php $this->extend('admin/_layout.php'); ?>
<?php $this->block('title', 'Admin Dashboard'); ?>
<?php $this->block('content'); ?>
<h1>Dashboard</h1>
<div class="stats">
<?= mini\render('admin/partials/stat-card.php', ['label' => 'Users', 'value' => $userCount]) ?>
<?= mini\render('admin/partials/stat-card.php', ['label' => 'Posts', 'value' => $postCount]) ?>
</div>
<?php $this->end(); ?>
JSON API with Template
// _routes/api/user.php
header('Content-Type: application/json');
echo render('api/user.json.php', ['user' => $user]);
// _views/api/user.json.php
<?= json_encode([
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
], JSON_PRETTY_PRINT) ?>