Templates
Auto-escaping output, control structures, layouts and sections, partials, and shared data.
Templates are app/templates/*.orbit.php. They compile to plain PHP once, then execute as fast as any other included file.
<?php
return $this->view->respond('articles/show', [
'title' => 'Reading list',
'article' => $article,
]);Output
{# Escaped. This is what you want essentially always. #}
<h1>{{ $title }}</h1>
{# Not escaped. Deliberately loud, so it stands out in review. #}
<div>{!! $renderedMarkdown !!}</div>
{# A comment. Removed at compile time; never reaches the browser. #}
{# Literal braces, for Vue, Angular, Handlebars and friends. #}
<span>@{{ notPhp }}</span>{{ }} is shorter and easier to type than {!! !!}. The safe form has to be the convenient one, or people reach for the other by habit. Escaping is not something a template remembers to ask for — it is what output is.
What values are allowed
Strings, numbers, booleans, null and Stringable render. Arrays and plain objects throw:
TemplateError: Cannot render a value of type array. Convert it in the handler
before passing it to the template.Better an error than the string Array appearing in a page, or a fatal from a bare cast.
Control structures
@if($articles === [])
<p>Nothing published yet.</p>
@elseif(count($articles) === 1)
<p>One article.</p>
@else
<p>{{ count($articles) }} articles.</p>
@endif
@foreach($articles as $article)
<article>
<h2>{{ $article['title'] }}</h2>
<p>{{ $article['excerpt'] }}</p>
</article>
@endforeach
@for($page = 1; $page <= $pages; $page++)
<a href="?page={{ $page }}">{{ $page }}</a>
@endfor
@while($row = $cursor->next())
<li>{{ $row['name'] }}</li>
@endwhileAn unrecognised @word passes through untouched — it is far more likely to be prose (an email address, a decorator in a code sample) than a typo'd directive.
Layouts and sections
{# app/templates/layout.orbit.php #}
<!doctype html>
<html lang="en">
<head>
<title>{{ $title }}</title>
@yield('head')
</head>
<body>
<main>@yield('content')</main>
</body>
</html>{# app/templates/articles/show.orbit.php #}
@extends('layout')
@section('head')
<meta name="description" content="{{ $article['excerpt'] }}">
@endsection
@section('content')
<h1>{{ $article['title'] }}</h1>
{!! $article['html'] !!}
@endsectionThe child renders first, collecting its sections; the layout renders second and pulls them out with @yield. Anything the child emits outside a section becomes the implicit content section, so a layout that yields only content works with a template that never declares one.
A section left unclosed is caught with the template and section named, rather than producing a blank page.
Partials
@include('partials/pagination')
{# Extra values are merged over the current ones #}
@include('partials/button')A partial sees the including template's data.
Shared data
Values every page needs are supplied once, at boot:
<?php
$templates = new TemplateEngine(
$root . '/app/templates',
$storage . '/cache/views',
alwaysRecompile: $debug,
shared: [
'appUrl' => $env->string('APP_URL', 'http://localhost:8080'),
'sapi' => PHP_SAPI,
'phpVersion' => PHP_VERSION,
],
);<meta property="og:image" content="{{ $appUrl }}/assets/brand/social-card.png">
<footer>Served by {{ $sapi }} on PHP {{ $phpVersion }}.</footer>Per-render data wins over shared data, so a page can override one value without affecting any other page.
A mutable bag on the engine would be per-request state living on a process-lifetime service — one page's values leaking into the next request's render. Supplying them at construction makes that impossible.
Compilation and caching
A template is compiled to PHP and cached under storage/cache/views. It recompiles when the source is newer, or on every render when alwaysRecompile is on — which --debug sets, so edits appear without clearing anything by hand.
Compiled files are written to a temporary name and renamed into place. A worker must never require a file another process is halfway through writing.
The staleness check compares file modification times, which a deploy method that preserves them (some rsync and tarball-extraction flows do) can defeat. orbit storage:clear deletes everything under storage/cache/views unconditionally — safe at any time, since the next render just recompiles what it finds missing.
Two things templates cannot do
Raw PHP tags are neutralised. A literal <? renders as text. Every executable construct comes from a directive the compiler knows about, which keeps templates readable and stops a stray tag from becoming a surprise.
Template names are validated, not sanitised. Names may contain letters, digits, underscores, hyphens and forward slashes:
<?php
$view->render('../../../etc/passwd');
// TemplateError: Invalid template name "../../../etc/passwd". Names may contain
// letters, digits, underscores, hyphens and forward slashes only.A name that reached the engine from a request could otherwise compile and execute an arbitrary file.
Rendering without a response
<?php
$html = $this->view->render('emails/welcome', ['name' => $user->name]);
$this->view->exists('emails/welcome'); // trueEscaping inside a template
{{ }} escapes for HTML text. For other contexts, call the escaper explicitly:
<a href="{!! PhpOrbit\Security\Escaper::urlAttribute($link) !!}">Open</a>
<button data-name="{!! PhpOrbit\Security\Escaper::attribute($name) !!}">Save</button>
<script>
const user = {!! PhpOrbit\Security\Escaper::js($name) !!};
</script>Note that js() includes its own quotes — do not add more. Escaping in detail →