Skip to content

Templates and rendering

Once an action has run and its view has decided what to show, a renderer turns a template into the bytes of the response. This page covers that last leg: the renderer, the template it runs, and how layouts and layers compose a page from nested pieces.

A few terms up front:

  • Renderer — the object that takes a template plus the view’s data and produces output. Quiote’s default runs plain PHP templates.
  • Template — the file the renderer runs (a .php file for the default renderer).
  • Layout / layer — a page is usually a layout built from one or more layers (an outer shell wrapping inner content), rather than a single template file.

Nothing here is opinionated about your front end — the default is PHP files, and swapping in another renderer is a config change, not a code change.

Rendering is the last real work in a request. It happens in the action phase of the middleware pipeline — you never call the renderer yourself; the framework does, once your view has run.

How the framework finds the renderer and template. Each output type (html, json, …) names a renderer in its output_types config. OutputTypeConfigHandler reads that config into an OutputType, and OutputType::getRenderer() instantiates the class you named (checking it extends Quiote\Renderer\Renderer). The template file is found by name: the action name plus the view name resolve to Modules/<Module>/Templates/<Action><View>.php (see Template location).

The path a request takes through it:

DispatchMiddleware runs the action through ActionExecutor → the action returns a view name → ActionExecutor resolves the view class and calls its execute<OutputType>() method (e.g. executeHtml()) → the view loads its layout and layers → each layer hands its template to the configured renderer → the rendered string is written to the PSR-7 response body.

For the complete picture see The request lifecycle and The middleware pipeline.

A renderer extends the abstract Quiote\Renderer\Renderer base class (its one required method is render()). The kernel ships only the plain-PHP renderer; the rest are opt-in packages — install the package, then name its class in the output type:

RendererTemplatesShips in
Quiote\Renderer\PhpRendererPlain PHP (.php) — the defaultthe kernel
Quiote\Renderer\Phptal\PhptalRendererPHPTALquioteframework/phptal
Quiote\Renderer\Xslt\XsltRendererXSLTquioteframework/xslt
Quiote\Renderer\Twig\TwigRendererTwigquioteframework/twig

You select the renderer per output type in output_types (see Output types):

// Config/output_types.php — inside the output type's array
'default_renderer' => 'php',
'renderers' => [
'php' => ['class' => \Quiote\Renderer\PhpRenderer::class],
],

The PhpRenderer exposes the view’s attributes to the template as a single array — $template by default:

<!DOCTYPE html>
<html lang="en">
<head>
<title><?php echo htmlspecialchars($template['title'] ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<h1><?php echo htmlspecialchars($template['post']['heading'], ENT_QUOTES, 'UTF-8'); ?></h1>
</body>
</html>

Where do those values come from? Everything the action set with setAttribute(), plus everything the view set, is visible in $template. The attribute the action set as post is $template['post'] in the template.

The variable name is configurable per renderer (var_name, default template), as are extract_vars (extract attributes into individual variables), slots_var_name, and default_extension.

Three things about that array are worth knowing:

  • It is the attributes array itself, not a wrapper around it. Reading a key the action never set is an ordinary undefined-key warning — which is how a typo in a template gets found, so the renderer does not soften it. Use ?? '' where a value is genuinely optional, as the title above does.
  • It is bound by reference, so a template writing to $template['x'] writes back to the attributes the view passed in — visible to the layers rendered after it.
  • moduleName and actionName are filled in from the layer being rendered, since the attributes carry neither. An attribute the action set under either name wins; this only fills a gap.

A view rarely renders a single file. A page is usually a layout made of layers — for example, an outer HTML shell wrapping an inner content layer. Layouts and their layers are declared in the output type:

// Config/output_types.php — inside the output type's array
'default_layout' => 'default',
'layouts' => [
'default' => [
'layers' => [
'content' => [],
],
],
],

In the view, loadLayout() reads this layout for the current output type and prepares its layers:

public function executeHtml(WebRequest $rd)
{
$this->loadLayout(); // prepare the "content" layer
$this->setAttribute('title', 'Home');
// returning null renders the prepared layers
}

When executeHtml() returns nothing but layers are loaded, the framework renders each layer in order. The output of an inner layer is passed to the next as $inner — so an outer shell layer can wrap the content:

<!-- shell layer template -->
<!DOCTYPE html>
<html>
<body>
<?php echo $inner; ?> <!-- rendered content layer -->
</body>
</html>

This is why loadLayout() matters: without it, executeHtml() returning nothing produces an empty body, because there are no layers to render.

Templates live in the module’s Templates/ directory, named after the action and view. An Index action returning the Success view renders Modules/Default/Templates/IndexSuccess.php. Keeping the names aligned is what keeps the wiring implicit — see Actions and views.

A view can render the output of another action inline — a “slot”. Use it for shared fragments like a navigation bar or a sidebar widget that has its own action and view. Two entry points on View, both canonical — one just wraps the other:

public function renderSlot(string $moduleName, string $actionName, ?array $arguments = null, ?string $outputType = null): string
{
$slotContent = $this->createSlotContent($moduleName, $actionName, $arguments, $outputType);
return $slotContent->getContent();
}

Pick based on what you need next:

  • renderSlot($module, $action, $arguments, $outputType) returns the rendered string immediately. Use this when you just want the HTML now, e.g. to store as a regular view attribute:

    public function executeHtml(WebRequest $rd)
    {
    $this->loadLayout();
    $this->setAttribute('nav', $this->renderSlot('Default', 'Navigation'));
    }
  • createSlotContent($module, $action, $arguments, $outputType) returns a SlotRenderable value object without rendering it immediately. Use this to attach the slot to a named layer slot, letting the renderer decide when/how to insert it into the template:

    $this->getLayer('content')->setSlot(
    'Attachments',
    $this->createSlotContent('Item', 'Attachment', ['item_id' => $item->getId()])
    );

Both are prepared by SlotMiddleware in the pipeline, so slots participate in the normal execution model rather than being a side channel.

$arguments is applied via setParameter() onto the shared request before the slot’s action/view runs, then restored afterward — explicit, developer-supplied values, not raw HTTP input, and (like any setParameter() call) auto-whitelisted for strict-access reads. What actually runs to read them depends on whether the slot action is isSimple():

  • The slot action is isSimple() — the common case; most slots are static or purely presentational. execute*() never runs at all, so only the view can read the passed arguments, via $rd->getParameter('currentPage') in execute()/executeHtml()/etc.
  • The slot action needs real business logic against those arguments — it must not be isSimple(). Give it real register{Method}Validators() for the parameter names it needs, the same as any other action; setParameter()’s auto-whitelisting already makes the arguments technically readable, but an isSimple() action has no code path left to read them from at all, and skipping real validators on a non-simple action forfeits the type/shape checking a validator would otherwise give you.

In short: renderSlot()/createSlotContent() for passing values in, isSimple() for whether any action code ever gets to see them once they’re there. A slot’s action/view runs as its own separate Action/View instance with its own private attributes, though — a slot can’t set an attribute that the top-level view or layout template will see. That isolation is exactly right for the slot’s own output, but wrong for one specific thing a slot-nested view often needs to do: register page-level CSS/JS. That’s what AssetRegistry is for.

A view — top-level or nested inside a slot — registers assets for the page’s <head> through two methods on View:

class ChartSuccessView extends View
{
public function executeHtml(WebRequest $rd)
{
$this->addCss('css/chart.css');
$this->addJavascript('js/d3.min.js');
$this->addJavascript('js/my-chart.js');
}
}

Unlike setAttribute(), there’s no immutability or isolation footgun here — call it and move on, no reassignment needed. addCss()/addJavascript() reach the request-scoped Quiote\Asset\AssetRegistry in the container directly rather than going through the view’s own attribute holder, so they work identically whether the view is the page’s top-level view or something rendered inside a slot: every node in the render tree resolves the same registry. The registry deduplicates at insertion time and preserves first-seen order — two different slots both needing d3.min.js render it once, at the position it was first needed.

Wire the registry into a renderer’s assigns to read it from a template, the same way the request is exposed as $rq:

// Config/output_types.php — inside the renderer's config
'assigns' => [
'request' => 'rq',
'asset_registry' => 'assets',
],

Then, typically in the layout’s shell template:

<?php foreach ($assets->css() as $href): ?>
<link rel="stylesheet" type="text/css" href="<?= $href ?>">
<?php endforeach; ?>
<?php foreach ($assets->javascript() as $src): ?>
<script type="text/javascript" src="<?= $src ?>"></script>
<?php endforeach; ?>

AssetRegistry has no priority/ordering beyond insertion order and no per-asset metadata (media queries, defer/async, integrity hashes) — reach for a small value object in place of the plain string href/src if you need those; there’s no built-in support for inline <script> blocks either. It’s request-scoped and cleared between requests in worker mode, the same as everything else covered in the worker-mode note.

Because the renderer is per output type, you can render HTML with PHP templates and, say, a document export with XSLT — in the same app — by declaring each output type with its own renderer. Nothing in the action or view changes; they set attributes and return content, and the configured renderer decides how that becomes bytes.