Rendering
Rendering
src/Renderer.php provides a micro template engine that separates logic from presentation. Templates receive pre-computed data and use helper methods for escaping, partials, and components.
Basic Usage
$r = new Bulletin\Renderer(__DIR__ . '/views');
$r->display('thread-clean', ['thread' => $thread, 'posts' => $posts]);
Template Helpers
Escaped Output
Always use $this->e() to output user content:
// In a template:
<h1><?= $this->e($thread['title']) ?></h1>
<p>By <?= $this->e($username) ?></p>
Raw Output (Trusted HTML)
For already-sanitized HTML (like rendered Markdown):
<div class="post-body"><?= $this->raw($renderedContent) ?></div>
Partials
Include reusable template fragments:
<?= $this->renderPartial('sidebar', ['categories' => $categories]) ?>
Components
Render a component with data:
<?= $this->renderComponent('post', [
'post' => $post,
'number' => 1,
'isOp' => true,
'thread' => $thread,
]) ?>
Slots and Layouts
Define layout sections:
<?php $this->slot('sidebar') ?>
<!-- sidebar content -->
<?php $this->endSlot() ?>
<!-- Later: -->
<?php $this->renderSlot('sidebar') ?>
<!-- Or with fallback: -->
<?php $this->yield('sidebar', 'Default content') ?>
Control Flow
<?= $this->when($isAdmin, fn() => $this->renderComponent('admin_panel')) ?>
<?= $this->each($posts, fn($post, $i) => $this->renderComponent('post', ['post' => $post, 'number' => $i + 1])) ?>
CSRF Field
<form method="POST">
<?= $this->csrfField() ?>
<!-- other fields -->
</form>
URL and Translation Helpers
<a href="<?= $this->url('thread', ['id' => $thread['id']]) ?>">View</a>
<button><?= $this->t('submit') ?></button>
Component Structure
Components live in views/components/:
views/
├── components/
│ ├── post.php # Single post display
│ └── thread_modals.php # Moderation dialogs
├── thread-clean.php # Clean thread template using components
└── header.php # Legacy header (shared)
Example Component (views/components/post.php)
<?php
/**
* @var Bulletin\Renderer $this
* @var array $post
* @var int $number
* @var bool $isOp
* @var array $thread
*/
?>
<article class="post <?= $isOp ? 'post-op' : '' ?>">
<div class="post-author"><?= $this->e($post['author']) ?></div>
<div class="post-body"><?= marked_parse($post['content']) ?></div>
</article>
Clean Template Example
<?php
/** @var Bulletin\Renderer $this */
/** @var array $thread */
/** @var array $posts */
?>
<?php include __DIR__.'/header.php'; ?>
<?php render_header($thread['title']); ?>
<h1><?= $this->e($thread['title']) ?></h1>
<?php foreach ($posts as $i => $post): ?>
<?= $this->renderComponent('post', [
'post' => $post,
'number' => $i + 2,
'isOp' => false,
'thread' => $thread,
]) ?>
<?php endforeach; ?>
<?php render_footer(); ?>
Best Practices
- No logic in templates — all data is pre-computed in action handlers
- Always escape — use
$this->e()for any user-generated content - Use components — extract reusable UI into
views/components/ - Keep templates declarative — if/else for visibility, foreach for loops, no business logic