Views
A view is a template under views/, named with dots for directories: home.index is views/home/index.twig with Twig. Controllers return one as an HTML response:
return view('home.index', ['user' => $user]);
view() renders through Crystoline\Xpsr\View\ViewHandlerInterface, which services can also take to render a string, for example for an email body:
$html = $views->render('emails.welcome', ['user' => $user]);
A view without a template throws Crystoline\Xpsr\View\ViewNotFoundException, whose view property names it.
Engines
Twig is the default. app.view_engine in config/app.php (or VIEW_ENGINE) picks another:
| Engine | app.view_engine |
Package | Template | Escaping |
|---|---|---|---|---|
| Twig | twig |
twig/twig (in the skeleton) |
views/home/index.twig |
automatic |
| Plates | plates |
crystoline/xpsr-view-plates |
views/home/index.php |
by hand, with $this->e() |
| Latte | latte |
crystoline/xpsr-view-latte |
views/home/index.latte |
automatic, by context |
| Blade | blade |
crystoline/xpsr-view-blade |
views/home/index.blade.php |
automatic, {{ }} |
composer require crystoline/xpsr-view-latte
// config/app.php
'view_engine' => env('VIEW_ENGINE', 'latte'),
app.view_dir is the template directory and app.view_cache_dir is where Twig, Latte, and Blade keep compiled templates (false: Twig and Latte compile on each request, Blade uses a directory under the system temporary directory). With app.debug off, Twig and Latte do not look for changed templates, so clear the cache directory on deploy.
Template functions
The view provider gives every engine the same functions:
| Function | Returns |
|---|---|
csrf_token() |
The session's CSRF token. |
csrf_field() |
A hidden _token input with it, as HTML. |
session('key', $default) |
A session value, such as a flash message. |
auth_user() |
The signed-in user, or null. |
can('update', $task) |
Whether the signed-in user may, by the gate's policies. |
The CSRF functions and session() need a route behind SessionMiddleware; see SESSIONS.md. auth_user() and can() need Authenticate (optional or required) on the route; see AUTH.md. Each engine calls them its own way:
| Engine | Form field | A value | A condition |
|---|---|---|---|
| Twig | {{ csrf_field() }} |
{{ session('status') }} |
{% if can('update', task) %} |
| Plates | <?= $this->csrf_field() ?> |
<?= $this->e($this->session('status')) ?> |
<?php if ($this->can('update', $task)): ?> |
| Latte | {csrf_field()} |
{session('status')} |
{if can('update', $task)} |
| Blade | @csrf or {{ $xpsr->csrf_field() }} |
{{ $xpsr->session('status') }} |
@if ($xpsr->can('update', $task)) |
An application adds its own through Crystoline\Xpsr\View\ViewFunctionsInterface, which every official engine implements, before the first render. A service registry's boot() is the place; it builds the view handler on each request, which costs little because templates are compiled only when they are rendered:
$views = $this->container->get(ViewHandlerInterface::class);
if ($views instanceof ViewFunctionsInterface) {
$views->addFunction('asset', static fn(string $path): string => '/build/' . $path);
$views->addFunction('icon', static fn(string $name): string => "<svg><use href=\"/icons.svg#{$name}\"/></svg>", html: true);
}
A function's result is escaped like any value unless it is added with html: true, which says it returns safe HTML. Plates escapes nothing by itself, so its templates call $this->e() for everything else.
Your own engine
app.view_engine can also name a class that implements ViewHandlerInterface (and ViewFunctionsInterface, for the template functions); the container builds it, so a factory in config/factory.php can give it its settings. Crystoline\Xpsr\Testing\ViewHandlerContractTestCase is the behavior every engine must have: extend it in the engine's tests, as the official packages do, and implement createHandler(), extension(), and templates().