Skip to main content

Overview

ForkBB uses a custom template engine that compiles templates to native PHP. The View system provides:
  • Template compilation to PHP with caching
  • Template inheritance (extends/blocks)
  • Automatic escaping for security
  • View composers for shared data
  • Theme support with fallback
Template files use the .forkbb.php extension and are located in app/templates/

View Class

The View class handles template compilation and rendering:
app/Core/View.php
class View
{
    protected string $ext = '.forkbb.php';
    protected string $cacheDir;        // Compiled templates
    protected string $defaultDir;      // Default theme
    
    public function __construct(string|array $config, mixed $views)
    
    // Render a page
    public function rendering(Page $p, bool $sendHeaders = true): ?string
    
    // Fetch a template
    public function fetch(string $name, array $data = []): string
    
    // Add template directories
    public function addTplDir(string $pathToDir, int $priority): View
    
    // Register view composer
    public function composer(string|array $name, mixed $composer): void
}

Template Locations

app/templates/
├── _default/           # Default theme (always available)
│   ├── layouts/
│   │   ├── main.forkbb.php
│   │   └── admin.forkbb.php
│   ├── index.forkbb.php
│   ├── forum.forkbb.php
│   ├── topic.forkbb.php
│   └── ...
└── _user/              # User theme (overrides _default)
    ├── style1/
    └── style2/
User templates in _user/ override default templates. The system checks user directories first, then falls back to _default/.

Template Syntax

Basic Template

index.forkbb.php
@extends('layouts/main')

@section('content')
<div class="main-content">
    <h1>{{ $p->pageTitle }}</h1>
    
    @if($p->user->isGuest)
        <p>Welcome, Guest!</p>
    @else
        <p>Hello, {{ e($p->user->username) }}!</p>
    @endif
    
    @foreach($p->forums as $forum)
        <div class="forum">
            <a href="{{ $forum->link }}">{{ e($forum->forum_name) }}</a>
            <p>{{ e($forum->forum_desc) }}</p>
        </div>
    @endforeach
</div>
@endsection

Template Inheritance

layouts/main.forkbb.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>{{ $p->pageTitle }}</title>
    @section('styles')
        <link rel="stylesheet" href="/style/main.css">
    @show
</head>
<body id="fork" class="{{ $p->identifier }}">
    @section('header')
        <header>
            <h1>{{ e($p->fTitle) }}</h1>
            @include('partials/navigation')
        </header>
    @show
    
    <main>
        @yield('content')
    </main>
    
    @section('footer')
        <footer>
            <p>&copy; 2024 Forum</p>
        </footer>
    @show
    
    @section('scripts')
        <script src="/js/common.js"></script>
    @show
</body>
</html>

Directives

Specify parent template for inheritance.
@extends('layouts/main')
Define or override a section.
@section('content')
    <div>Content here</div>
@endsection
Display a section’s content.
@yield('content')
End section and display it (for layout sections).
@section('header')
    <header>...</header>
@show
Include parent section’s content.
@section('scripts')
    @parent
    <script src="/js/my-script.js"></script>
@endsection
Include another template.
@include('partials/navigation')
@include('partials/post', ['post' => $post])

Control Structures

<!-- If statements -->
@if($condition)
    <p>True</p>
@elseif($other)
    <p>Other</p>
@else
    <p>False</p>
@endif

<!-- Unless -->
@unless($user->isGuest)
    <p>Logged in</p>
@endunless

<!-- Loops -->
@foreach($items as $item)
    <div>{{ e($item) }}</div>
@endforeach

@for($i = 0; $i < 10; $i++)
    <p>{{ $i }}</p>
@endfor

@while($condition)
    <p>Looping...</p>
@endwhile

<!-- Empty check -->
@forelse($posts as $post)
    <div>{{ e($post->message) }}</div>
@empty
    <p>No posts found</p>
@endforelse

Output and Escaping

Always escape user-generated content to prevent XSS attacks!
<!-- Escaped output (safe) -->
{{ e($user->username) }}
{{ e($post->message) }}

<!-- Raw output (dangerous - only for trusted content) -->
{!! $p->htmlContent !!}

<!-- Function call -->
{{ e($c->Func->friendly($name)) }}

<!-- Ternary -->
{{ $user->isAdmin ? 'Admin' : 'User' }}
The e() function escapes HTML entities:
function e(mixed $value): string {
    return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

Accessing Page Data

The page object is available as $p:
<!-- Page properties -->
{{ e($p->pageTitle) }}
{{ e($p->fTitle) }}
{{ $p->canonical }}

<!-- User data -->
{{ e($p->user->username) }}
{{ $p->user->isAdmin }}
{{ $p->user->id }}

<!-- Page-specific data -->
@foreach($p->forums as $forum)
    {{ e($forum->forum_name) }}
@endforeach

@foreach($p->topics as $topic)
    {{ e($topic->subject) }}
@endforeach

Partials and Components

Including Partials

<!-- Simple include -->
@include('partials/header')

<!-- Include with data -->
@include('partials/post', [
    'post' => $post,
    'showActions' => true
])

<!-- Conditional include -->
@if($showSidebar)
    @include('partials/sidebar')
@endif

Reusable Components

partials/topic-row.forkbb.php
<div class="topic-row">
    <div class="topic-icon">
        @if($topic->icon)
            <img src="{{ $topic->icon }}" alt="">
        @endif
    </div>
    
    <div class="topic-main">
        <h3>
            <a href="{{ $topic->link }}">{{ e($topic->subject) }}</a>
        </h3>
        <p class="topic-meta">
            By {{ e($topic->poster) }}
            on {{ date('Y-m-d H:i', $topic->posted) }}
        </p>
    </div>
    
    <div class="topic-stats">
        <span>{{ $topic->num_replies }} replies</span>
        <span>{{ $topic->num_views }} views</span>
    </div>
    
    <div class="topic-last">
        @if($topic->last_post)
            <a href="{{ $topic->linkLast }}">Last Post</a>
        @endif
    </div>
</div>
Usage:
@foreach($p->topics as $topic)
    @include('partials/topic-row', ['topic' => $topic])
@endforeach

View Composers

View composers inject data into templates automatically:
config/main.dist.php
'View' => [
    'class' => \ForkBB\Core\View::class,
    'config' => [
        'composers' => [
            '*' => function($view) {
                // Available in all templates
                return [
                    'siteName' => 'My Forum',
                    'year' => date('Y'),
                ];
            },
            'forum*' => function($view) {
                // Available in forum.forkbb.php and forum-*.forkbb.php
                return [
                    'forumSpecificData' => '...',
                ];
            },
        ],
    ],
]

Template Compilation

Templates are automatically compiled to PHP and cached:
1

First Request

Template is compiled to PHP and cached in app/cache/
2

Subsequent Requests

Cached PHP file is used (fast!)
3

Template Changed

Cache is automatically invalidated and recompiled

Compiled Template Example

Original Template
@extends('layouts/main')

@section('content')
    <h1>{{ e($p->pageTitle) }}</h1>
    @foreach($p->items as $item)
        <div>{{ e($item->name) }}</div>
    @endforeach
@endsection
Compiled Output (cached)
<?php $this->extend('layouts/main'); ?>

<?php $this->beginBlock('content'); ?>
    <h1><?= e($p->pageTitle) ?></h1>
    <?php foreach($p->items as $item): ?>
        <div><?= e($item->name) ?></div>
    <?php endforeach; ?>
<?php $this->endBlock(); ?>

Rendering Process

The view rendering process:
// In controller
$page->nameTpl = 'forum';  // Set template name
return $page;

// In bootstrap.php
$tpl = $c->View->rendering($page);
exit($tpl);

// View class
public function rendering(Page $p, bool $sendHeaders = true): ?string
{
    if (null === $p->nameTpl) {
        return null;  // No template
    }
    
    // Compile template if needed
    $compiled = $this->prepare($p->nameTpl);
    
    // Execute compiled template
    require $compiled;
    
    // Return rendered HTML
    return $this->block('content');
}

Creating Custom Templates

Step 1: Create Template File

app/templates/_default/my-page.forkbb.php
@extends('layouts/main')

@section('content')
<div class="my-page">
    <h1>{{ e($p->heading) }}</h1>
    
    @if($p->items)
        <ul>
            @foreach($p->items as $item)
                <li>{{ e($item) }}</li>
            @endforeach
        </ul>
    @else
        <p>No items found.</p>
    @endif
</div>
@endsection

Step 2: Use in Controller

public function myAction(array $args, string $method): Page
{
    $page = $this->c->MyPage->create();
    $page->nameTpl = 'my-page';  // References my-page.forkbb.php
    $page->heading = 'My Custom Page';
    $page->items = ['Item 1', 'Item 2', 'Item 3'];
    
    return $page;
}

Best Practices

Use e() for all user-generated content.
<!-- Good -->
{{ e($user->username) }}

<!-- Bad -->
{{ $user->username }}
Templates should display data, not contain business logic.
<!-- Good -->
@if($topic->canReply)
    <button>Reply</button>
@endif

<!-- Bad -->
@if($topic->closed == 0 && $user->g_post_replies == 1)
    <button>Reply</button>
@endif
Extract repeated markup into partials.
@include('partials/user-badge', ['user' => $post->user])
Use layouts to avoid repeating HTML structure.
@extends('layouts/main')
@section('content')
    <!-- Only page-specific content -->
@endsection

Next Steps

Controllers

Pass data to templates from controllers

Models

Display model data in templates