Templates & Views
No Blade. No Twig. No Mustache. PHP IS the template engine. ZealPHP templates are plain .php files — loops, conditionals, expressions, classes, everything you know works. Zero learning curve, full language power.
Pass data, render a template
$app->route('/users/{id}', function($id) {
$user = User::find($id);
if (!$user) return 404;
App::render('profile', [
'user' => $user,
'posts' => $user->posts(),
'isAdmin' => $user->role === 'admin',
]);
});
<h1><?= htmlspecialchars($user->name) ?></h1>
<?php if ($isAdmin): ?>
<span class="badge">Admin</span>
<?php endif; ?>
<h2>Posts (<?= count($posts) ?>)</h2>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="/post/<?= $post->id ?>">
<?= htmlspecialchars($post->title) ?>
</a>
<small><?= $post->created_at ?></small>
</li>
<?php endforeach; ?>
</ul>
Every key in the $args array becomes a local variable in the template via extract(). No magic syntax — just PHP.
Layouts & composition
Templates can render other templates. Build a layout system with a single master template and components:
<?php use ZealPHP\App;
App::render('_master', ['title' => 'About Us', 'page' => 'about']);
<!doctype html>
<html>
<head><title><?= htmlspecialchars($title) ?></title></head>
<body>
<?php App::render('_nav', ['active' => $page]) ?>
<main>
<?php App::render("/pages/$page") ?>
</main>
<?php App::render('_footer') ?>
</body>
</html>
public/ is 3 lines that call App::render('_master', [...]). The master template renders the nav, the page content, and the footer. No template inheritance syntax needed — it's just PHP includes.
Components with slots
Reusable UI components that accept data as arguments:
<div class="card">
<div class="card-icon"><?= $icon ?></div>
<h3><?= htmlspecialchars($title) ?></h3>
<p><?= htmlspecialchars($body) ?></p>
<?php if (!empty($href)): ?>
<a href="<?= htmlspecialchars($href) ?>">Read more</a>
<?php endif; ?>
</div>
<?php foreach ($features as $f): ?>
<?php App::render('/components/_card', [
'icon' => $f['icon'],
'title' => $f['name'],
'body' => $f['desc'],
'href' => $f['url'],
]) ?>
<?php endforeach; ?>
Path resolution
| Call | Resolves to | When |
|---|---|---|
App::render('home') | template/home.php | Top-level template |
App::render('/components/_card') | template/components/_card.php | Leading / = absolute from template/ |
App::render('header') from public/users.php | template/users/header.php | Auto-namespaces by current public file |
App::render('header') (fallback) | template/header.php | If namespaced path doesn't exist |
Three render methods
| Method | Returns | Use when |
|---|---|---|
App::render($tpl, $args) | void (echoes) | Direct output in route handler or another template |
App::renderToString($tpl, $args) | string | Need HTML as value — email, cache, or yield |
App::renderStream($tpl, $args) | Generator | SSR streaming — works with both regular and streaming templates |
SSR Streaming — yield from templates
App::renderStream() returns a Generator. If the template file returns a Generator (via IIFE), it delegates with yield from. If the template echoes normally, the output is captured and yielded as one chunk. Both patterns compose in the same streaming pipeline.
<?php
// Declare what data this template needs —
// framework injects by name (like route handlers)
return function($users) {
yield "<section class='users'>";
foreach ($users as $user) {
yield "<div class='card'>"
. htmlspecialchars($user->name)
. "</div>\n";
}
yield "</section>";
};
$app->route('/users', function() {
return (function() {
// Regular template → single chunk
yield from App::renderStream(
'shell-open', ['title' => 'Users']
);
// Streaming template → per-user chunks
yield from App::renderStream(
'users/stream',
['users' => User::all()]
);
yield from App::renderStream('shell-close');
})();
});
The template declares function($users) — the framework injects $users from the args array by name, exactly like route parameter injection. Each yield flushes to the browser immediately.
Three streaming template styles
| Style | Template code | Best for |
|---|---|---|
| Closure (cleanest) | return function($users) { yield ...; }; |
New streaming templates — parameter injection, no use() needed |
| IIFE Generator | return (function() use ($users) { yield ...; })(); |
When you need variables from the include scope via use() |
| Regular echo | <h1><?= $title ?></h1> |
Non-streaming templates — output captured as one chunk |
All three compose in the same yield from App::renderStream(...) pipeline.
Yield from everywhere
Generators work in route handlers, public/ files, API handlers, and template files:
| Location | How to stream | Example |
|---|---|---|
| Route handler | Return a Generator directly | return (function() { yield "chunk"; })(); |
| Public file | Return a Generator from the file | public/feed.php → <?php return (function() { yield "..."; })(); |
| API handler | Return a Generator from $get/$post |
$get = function() { return (function() { yield ...; })(); }; |
| Template | Return a Closure via renderStream() |
return function($items) { yield ...; }; |
<?php
// File: public/feed.php → GET /feed
// Returns a Generator — framework streams each yield to the browser
use ZealPHP\App;
return (function() {
yield App::renderToString('shell-open', ['title' => 'Live Feed']);
yield "<h1>Feed</h1>";
foreach (fetchFeedItems() as $item) {
yield "<article>{$item->title}</article>\n";
}
yield App::renderToString('shell-close');
})();
<?php
// File: api/events/stream.php → GET /api/events/stream
$stream = function() {
return (function() {
yield '{"events":[';
$first = true;
foreach (Event::cursor() as $event) {
if (!$first) yield ',';
yield json_encode($event);
$first = false;
}
yield ']}';
})();
};
PHP template patterns cheat sheet
| Pattern | PHP |
|---|---|
| Output a variable | <?= $name ?> |
| Escape HTML | <?= htmlspecialchars($input) ?> |
| Conditional | <?php if ($cond): ?> ... <?php endif; ?> |
| Loop | <?php foreach ($items as $i): ?> ... <?php endforeach; ?> |
| Include component | <?php App::render('/components/_card', $args) ?> |
| Ternary default | <?= $subtitle ?? 'Default' ?> |
| Format number | <?= number_format($price, 2) ?> |
| Date format | <?= date('M j, Y', strtotime($created)) ?> |
| Raw HTML (trusted) | <?= $trusted_html ?> |
| JSON encode | <script>const data = <?= json_encode($data) ?></script> |
htmlspecialchars(). PHP templates have no auto-escaping — you get full control, which means full responsibility.
Why PHP over Blade/Twig/Mustache?
No new syntax. If you know PHP, you know the template engine.
Classes, closures, exceptions, generators — not a subset.
No cache directory. Templates are interpreted directly.
Autocompletion, type checking, refactoring — all free.
Templates can
yield. Progressive rendering built in.Render inside render. No "extends", no "blocks" — just function calls.