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. app.view_cache_dir is where Twig, Latte, and Blade keep compiled templates. In new projects it is var/cache/views. With false, Twig and Latte compile on every request, and Blade uses a directory under the system temporary directory. Give the cache a directory of its own, because php xpsr view:clear empties it.

A changed template is compiled again on the next request, whether app.debug is on or off:

  • Twig and Blade compare the template's file time with the compiled copy's. This costs almost nothing (about 0.01 ms per page for Twig). A deploy that keeps the files' original times needs php xpsr view:clear, for example unpacking an archive or rsync -a. See DEPLOYMENT.md.
  • Latte compares each template's content, so file times never matter. This costs about 0.2 ms for a page of three templates. To skip the check, see its README.
  • Plates templates are PHP files, which OPcache caches, so Plates uses no cache directory.

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().