Koldy PHP Framework — View & Templating
Koldy\Response\View is the framework's templating layer. There is no custom template DSL: a view file is a plain PHP file with a .phtml extension that is rendered through ob_start / require / ob_get_clean. You get includes, conditionals, loops, and data access with PHP itself — no parser, no compile cache, no new syntax to learn.
View File Location & Config
All view files are resolved relative to the path defined by the view key inside the paths array passed to Application::useConfig(). If the key is omitted, it defaults to <application_path>/views/. Every file must end with .phtml.
// public/index.php
Application::useConfig([
'application_path' => __DIR__ . '/application',
'storage_path' => __DIR__ . '/storage',
'public_path' => __DIR__ . '/public',
'paths' => [
'view' => __DIR__ . '/application/views/',
],
// ...
]);
You can read the resolved path at runtime:
Application::getViewPath(); // /.../application/views/
Application::getViewPath('pages/home.phtml'); // /.../application/views/pages/home.phtml
Returning a View from a Controller
View::create() is the static factory. Data is attached with the fluent Data trait API (set, setData, addData).
use Koldy\Response\View;
public function indexAction(): View
{
return View::create('pages/home')
->set('title', 'Welcome')
->set('user', $user);
}
Passing an entire associative array at once:
return View::create('pages/home')->setData([
'title' => 'Welcome',
'user' => $user,
'items' => $items,
]);
set(string $key, mixed $value): static— set one value.setData(array $data): static— replace all data.addData(array $data): static— merge with existing data.get(string $key): mixed/has(string $key): bool/delete(string $key): static.
Accessing Controller Data Inside a View
Inside a .phtml file, $this is the View instance, so every value attached in the controller is reachable as a property, thanks to the Data trait's __get magic.
<!-- application/views/pages/home.phtml -->
<!doctype html>
<html>
<head><title><?= htmlspecialchars($this->title) ?></title></head>
<body>
<h1>Hello, <?= htmlspecialchars($this->user->name) ?></h1>
<?php if ($this->has('flashMessage')): ?>
<div class="flash"><?= htmlspecialchars($this->flashMessage) ?></div>
<?php endif; ?>
<?php $this->printIf('footerNote'); // prints $this->footerNote if set ?>
</body>
</html>
Useful helpers:
$this->has('key')— true if the key exists.$this->get('key')— value ornull.$this->printIf('key')— echoes$this->$keywhen set, does nothing otherwise.
Always escape user data with htmlspecialchars() (or your own helper) before echoing into HTML.
Embedding One View Inside Another
Use $this->render() from inside a .phtml file to include a partial. The return value is the rendered string — echo it.
<!-- application/views/pages/home.phtml -->
<?= $this->render('partials/header') ?>
<main>
<h1><?= htmlspecialchars($this->title) ?></h1>
</main>
<?= $this->render('partials/footer') ?>
Inside the partial, $this is still the outer View instance, so anything set on the outer view is available:
<!-- application/views/partials/header.phtml -->
<header>
<a href="/"><?= htmlspecialchars($this->siteName) ?></a>
</header>
When the partial is optional and you do not want to throw if the file is missing, use renderViewIf():
<?= $this->renderViewIf('partials/optional-banner') ?>
renderViewIf() returns an empty string when the file does not exist. render() throws Koldy\Response\Exception if the file is missing.
Passing Local Variables to a Sub-View
The second argument of render() / renderViewIf() is an associative array. Each key is extracted as a local variable inside the sub-view (not a property of $this). Keys must be strings.
<?= $this->render('partials/user-card', [
'user' => $user,
'highlight' => true,
]) ?>
<!-- application/views/partials/user-card.phtml -->
<div class="user-card<?= $highlight ? ' user-card--highlight' : '' ?>">
<strong><?= htmlspecialchars($user->name) ?></strong>
<span><?= htmlspecialchars($user->email) ?></span>
</div>
Inside user-card.phtml, $user and $highlight are plain local variables. Data attached to the outer View is still reachable as $this->... in the partial.
Iterating Over an Array
Plain PHP. No special loop syntax.
<!-- application/views/pages/users.phtml -->
<ul>
<?php foreach ($this->users as $user): ?>
<li><?= htmlspecialchars($user->name) ?></li>
<?php endforeach; ?>
</ul>
Iterating and Delegating Each Element to Its Own Sub-View
Combine the loop with render() to keep row markup in its own file:
<!-- application/views/pages/users.phtml -->
<h1>Users</h1>
<ul class="user-list">
<?php foreach ($this->users as $user): ?>
<?= $this->render('partials/user-card', [
'user' => $user,
'highlight' => $user->isAdmin,
]) ?>
<?php endforeach; ?>
</ul>
<!-- application/views/partials/user-card.phtml -->
<li class="user-card<?= $highlight ? ' is-admin' : '' ?>">
<img src="<?= htmlspecialchars($user->avatar) ?>" alt="">
<div>
<strong><?= htmlspecialchars($user->name) ?></strong>
<small><?= htmlspecialchars($user->email) ?></small>
</div>
</li>
Each iteration renders partials/user-card.phtml with its own $user and $highlight locals. The partial has no knowledge of the outer loop — it only knows its inputs, which keeps partials reusable.
For a view name that is itself dynamic (e.g. stored on the controller), use renderViewInKeyIf():
// controller
return View::create('pages/dashboard')->set('widgetView', 'widgets/chart');
// pages/dashboard.phtml
<?= $this->renderViewInKeyIf('widgetView', ['data' => $this->chartData]) ?>
Checking If a View Exists
if (View::exists('partials/user-card')) {
// ...
}
exists() is static and returns true when the resolved .phtml file is present.
Custom View Roots
Override the base directory on a single View instance with setViewPath(). Useful for e-mail templates or anything that lives outside application/views/:
$mail = View::create('welcome')
->setViewPath(Application::getStoragePath('mail-views'))
->set('user', $user);
Rendering Without Flushing
Sometimes you want the rendered HTML as a string — for e-mail bodies, PDF generation, or embedding inside another response. Use getOutput() or cast the view to a string:
$html = View::create('emails/welcome')
->set('user', $user)
->getOutput();
// equivalent
$html = (string) View::create('emails/welcome')->set('user', $user);
getOutput() does not send headers or flush to the client; it only renders.
Response Lifecycle
View extends AbstractResponse, so the standard response API is available:
return View::create('pages/home')
->set('title', 'Welcome')
->setHeader('X-Frame-Options', 'DENY')
->statusCode(200)
->before(function () {
// runs immediately before output is flushed
})
->after(function () {
// runs after the response is sent to the client
});
When flush() is called (by the framework, usually), the buffered HTML is measured and a Content-Length header is added automatically (except for 1xx and 204 responses).
Why No Template Engine
PHP is already a compiled, opcode-cached templating language. Any alternative syntax — Blade, Twig, Smarty — adds a parse/compile step, a learning curve, and hides nothing PHP cannot express directly. The View class gives you every feature a templating layer offers: includes, loops, conditionals, data access, output capture, and existence checks — with zero translation cost and no dependencies. Your editor already knows PHP; your profiler already understands PHP; there is no second language in the pipeline.