Views
Overview
Views in Tempest use a syntax that can be thought of as a superset of HTML. Under the hood, PHP 8.4's DOM parser is used to parse templates before compiling them.
If you prefer using a templating engine with more widespread support, you may also use Blade, Twig, or any other—as long as you provide a way to initialize it.
Syntax overview
The following is an example of a view that inherits the x-base
component, passing a title
property.
Inside, a x-post
component is rendered multiple times thanks to a foreach loop on $this->posts
. That component has a default slot, in which the post details are rendered. The control flow is implemented using HTML attributes that start with colons :
.
<x-base title="Home"> <x-post :foreach="$this->posts as $post"> {!! $post->title !!} <span :if="$this->showDate($post)"> {{ $post->date }} </span> <span :else> - </span> </x-post> <div :forelse> <p>It's quite empty here…</p> </div> <x-footer /> </x-base>
Rendering views
As specified in the documentation about sending responses, views may be returned from controller actions using the view
function. This function is a shorthand for instantiating a Tempest\View\View
object.
use Tempest\Router\Get; use Tempest\View\View; use function Tempest\view; final readonly class AircraftController { #[Get(uri: '/aircraft/{aircraft}')] public function show(Aircraft $aircraft): View { return view('aircraft.view.php', aircraft: $aircraft); } }
View paths
The view
function accepts the path to a view as its first parameter. This path may be relative or absolute, depending on your preference.
The following three examples are equivalent:
return view(__DIR__ . '/views/home.view.php'); return view('./views/home.view.php'); return view('views/home.view.php');
Using dedicated view objects
A view object is a dedicated class that represent a specific view.
Using view objects will improve static insights in your controllers and view files, and may offer more flexibiltiy regarding how the data may be constructed before being passed on to a view file.
final class AircraftController { #[Get('/aircraft/{type}/{aircraft}')] public function show(AircraftType $type, Aircraft $aircraft): AircraftView { return new AircraftView($aircraft, $type); } }
To create a view object, implement the Tempest\View\View
interface, and add the Tempest\View\IsView
trait, which provides the default implementation.
use Tempest\View\View; use Tempest\View\IsView; final class AircraftView implements View { use IsView; public function __construct( public Aircraft $aircraft, public AircraftType $type, ) { $this->path = root_path('src/Aircraft/aircraft.view.php'); } }
In a view file rendered by a view object, you may add a type annotation for $this
. This allows IDEs like PhpStorm to infer variables and methods.
<?php /** @var \App\Modules\Home\HomeView $this */ ?> <p :if="$this->type === AircraftType::PC24"> The {{ $this->aircraft->icao_code }} is a light business jet produced by Pilatus Aircraft of Switzerland. </p>
View objects are an excellent way of encapsulating view-related logic and complexity, moving it away from controllers, while simultaneously improving static insights.
Templating syntax
Text interpolation
Text interpolation is done using the "mustache" syntax. This will escape the given variable or PHP expression before rendering it.
<span>Welcome, {{ $username }}</span>
To avoid escaping the data, you may use the following syntax. This should only be used on trusted, sanitized data, as this can open the door to an XSS vulnerability:
<div> {!! $content !!} </div>
Expression attributes
Expression attributes are HTML attributes that are evaluated as PHP code. Their syntax is the same as HTML attributes, except they are identified by a colon :
:
<html :lang="$this->user->language"></h1> <!-- <html lang="en"></h1> -->
As with text interpolation, only variables and PHP expressions that return a value are allowed. Mustache and PHP opening tags cannot be used inside them:
<!-- This is invalid --> <h1 :title="<?= $this->post->title ?>"></h1>
When using expression attributes on normal HTML elements, only scalar and Stringable
values can be returned. However, any object can be passed down to a component.
Boolean attributes
The HTML specification describes a special kind of attributes called boolean attributes. These attributes don't have a value, but indicate true
whenever they are present.
Using an expression attribute that return a boolean variable will follow the HTML specification, effectively not rendering the attribute if the value is false
.
<option :value="$value" :selected="$selected">{{ $label }}</option>
Depending on whether $selected
evaluates to true
or false
, the above example may or may not render the selected
attribute.
Control flow directives
:if
, :elseif
and :else
The :if
directive can conditionally render the element it is attached to, depending on the result of its expression. Similarly, the :elseif
and :else
directives can be used on direct siblings for additional control.
<span :if="$this->pendingUploads->isEmpty()">Import files</span> <span :else>Import {{ $this->pendingUploads->count() }} file(s)</span>
:foreach
and :forelse
The :foreach
directive may be used to render the associated element multiple times based on the result of its expression. Combined with :forelse
, an empty state can be displayed when the data is empty.
<li :foreach="$this->reports as $report"> {{ $report->title }} </li> <li :forelse> There is no report. </li>
Templates
The built-in <x-template>
element may be used as a placeholder when you want to use a directive without rendering an actual element in the DOM.
<x-template :foreach="$posts as $post"> <div>{{ $post->title }}</div> </x-template>
The example above will only render the child div
elements:
<div>Post A</div> <div>Post B</div> <div>Post C</div>
View components
Components allow for splitting the user interface into independent and reusable pieces.
Tempest doesn't have a concept of extending other views. Instead, a component may include another component using the same syntax as other HTML elements.
Registering view components
To create a view component, create a .view.php
file that starts with x-
. These files are referred to as anonymous view components and are automatically discovered by Tempest.
<html lang="en"> <head> <title :if="$title">{{ $title }} — AirAcme</title> <title :else>AirAcme</title> </head> <body> <x-slot /> </body> </html>
Using view components
All views may include a views components. In order to do so, you may simply use a component's name as a tag, including the x-
prefix:
<x-base :title="$this->post->title"> <article> {{ $this->post->body }} </article> </x-base>
The example above demonstrates how to pass data to a component using an expression attribute, as well as how pass elements as children if that component where the <x-slot />
tag is used.
Attributes in components
Attributes and expression attributes may be passed into view components. They work the same way as normal elements, and their values will be available in variables of the same name:
<x-base :title="$this->post->title"> // ... </x-base>
// ... <title :if="$title">{{ $title }}</title>
Note that the casing of attributes will affect the associated variable name:
camelCase
andPascalCase
attributes will be converted to$lowercase
variableskebab-case
andsnake_case
attributes will be converted to$camelCase
variables.
The idiomatic way of using attributes is to always use kebab-case
.
Fallthrough attributes
When class
and style
attributes are used on a view component, they will automatically be added to the root node, or merged with the existing attribute if it already exists.
<button class="rounded-md px-2.5 py-1.5 text-sm"> <!-- ... --> </button>
The example above defines a button component with a default set of classes. Using this component and providing another set of classes will merge them together:
<x-button class="text-gray-100 bg-gray-900" />
Similarly, the id
attribute will always replace an existing id
attribute on the root node of a view component.
Dynamic attributes
An $attributes
variable is accessible within view components. This variable is an array that contains all attributes passed to the component, except expression attributes.
Note that attributes names use kebab-case
.
<span class="px-2 py-1 rounded-md text-sm bg-gray-100 text-gray-900"> {{ $attributes['value'] }} </span>
Using slots
The content of components is often dynamic, depending on external context to be rendered. View components may define zero or more slot outlets, which may be used to render the given HTML fragments.
<button class="rounded-md px-2.5 py-1.5 text-sm text-gray-100 bg-gray-900"> <x-slot /> </button>
The example above defines a button component with default classes, and a slot inside. This component may be used like a normal HTML element, providing the content that will be rendered in the slot outlet:
<x-button> <!-- This will be injected into the <x-slot /> outlet --> <x-icon name="tabler:x" /> <span>Delete</span> </x-button>
Named slots
When a single slot is not enough, names can be attached to them. When using a component with named slot, you may use the <x-slot>
tag with a name
attribute to render content in a named outlet:
<html lang="en"> <head> <!-- … --> <x-slot name="styles" /> </head> <body> <x-slot /> </body> </html>
The above example uses a slot named styles
in its <head>
element. The <body>
element has a default, unnamed slot. A view component may use <x-base>
and optionally refer to the styles
slot using the syntax mentionned above, or simply provide content that will be injected in the default slot:
<x-base title="Hello World"> <!-- This part will be injected into the "styles" slot --> <x-slot name="styles"> <style> body { /* … */ } </style> </x-slot> <!-- Everything not living in a slot will be injected into the default slot --> <p> Hello World </p> </x-base>
Dynamic slots
Within a view component, a $slots
variable will always be provided, allowing you to dynamically access the named slots within the component.
This variable is an instance of Tempest\View\Slot
, with has a handful of properties:
$slot->name
: the slot's name$slot->content
: the compiled content of the slot$slot->attributes
: all the attributes defined on the slot$slot->{attribute}
: dynamically access an attribute defined on the slot
For instance, the snippet below implements a tab component that accepts any number of tabs.
<div :foreach="$slots as $slot"> <h1 :title="$slot->title">{{ $slot->name }}</h1> <p>{!! $slot->content !!}</p> </div>
<x-tabs> <x-slot name="php" title="PHP">This is the PHP tab</x-slot> <x-slot name="js" title="JavaScript">This is the JavaScript tab</x-slot> <x-slot name="html" title="HTML">This is the HTML tab</x-slot> </x-tabs>
View component classes
While anonymous components are useful on their own, there is sometimes the need to have more logic regarding the rendering of an element.
By creating a class that implements Tempest\View\ViewComponent
, you may affect the rendering process of a component. As with everything in Tempest, class components are automatically discovered and registered.
use Tempest\View\Elements\ViewComponentElement; use Tempest\View\ViewComponent; final readonly class ViteTagsComponent implements ViewComponent { public function __construct( private ViteConfig $viteConfig, ) { } public static function getName(): string { return 'x-vite-tags'; } public function compile(ViewComponentElement $element): string { $entrypoints = match (true) { $element->hasAttribute('entrypoints') => '$entrypoints', default => var_export($this->viteConfig->entrypoints, return: true), }; return <<<HTML <?= \Tempest\\vite_tags({$entrypoints}) ?> HTML; } }
The above is a simplified implementation of the built-in <x-vite-tags />
. The compile
method is expected to return normal PHP, that will not be parsed again by the templating engine.
Built-in components
Besides components that you may create yourself, Tempest provides a default set of useful built-in components to improve your developer experience.
x-icon
This component provides the ability to inject any icon from the Iconify project in your templates.
<x-icon name="material-symbols:php" class="size-4 text-indigo-400" />
The first time a specific icon is being rendered, Tempest will query the Iconify API to fetch the corresponding SVG tag. The result of this query will be cached indefinitely, so it can be reused at no furthemr cost.
Iconify has a large collection of icon sets, which you may browse using the Icônes directory.
x-vite-tags
Tempest has built-in support for Vite, the most popular front-end development server and build tool. You may read more about asset bundling in the dedicated documentation.
This component simply inject registered entrypoints where it is called.
<html lang="en"> <head> <x-vite-tags /> </head> <!-- ... --> </html>
Optionally, it accepts an entrypoint
attribute. If it is passed, the component will not inject other entrypoints discovered by Tempest.
<x-vite-tags entrypoint="src/main.ts" />
View processors
In most applications, a lot of views will need access to common data. To avoid having to manually provide this data to views through controller methods, it is possible to use view processors to manipulate views before they are rendered.
To create a view processor, implement the Tempest\View\ViewProcessor
interface on a class. This interface requires a process
method, which accepts and returns the view that will be rendered.
use Tempest\View\View; use Tempest\View\ViewProcessor; final class StarCountViewProcessor implements ViewProcessor { public function __construct( private readonly GitHub $github, ) {} public function process(View $view): View { if (! $view instanceof WithStargazersCount) { return $view; } return $view->data(stargazers: $this->github->getStarCount()); } }
The example above provides the $stargazers
variable to all view classes that implement the WithStargazersCount
interface.
View caching
Tempest views are always compiled to plain PHP code before being rendered. During development, this is done on-the-fly, every time. In production, these compiled views should be cached to avoid the performance overhead. This is done by setting the VIEW_CACHE
environment variable:
VIEW_CACHE=true
During deployments, that cache must be cleared in order to not serve outdated views to users. You may do that by running tempest view:clear
on every deploy. You can read more about caching and view caching in the cache chapter.
Using other engines
While Tempest View is simple to use, it currently lacks tooling support from editors and IDEs. You may also simply prefer other templating engines. For these reasons, you may use any other engine or your choice.
Out-of-the-box, Tempest has support for Twig and Blade. Note that the view loaders for other engines are not based on Tempest's discovery, so the syntax to refer to a specific view might differ.
Using Twig
You will first need to install the Twig engine. It is provided by the twig/twig
package:
composer require twig/twig
The next step is to provide the configuration needed for Twig to find your view files.
return new TwigConfig( viewPaths: [ __DIR__ . '/views/', ], );
Finally, update the view configuration to use the Twig renderer:
return new ViewConfig( rendererClass: \Tempest\View\Renderers\TwigViewRenderer::class, );
Using Blade
You will first need to install the Blade engine. Tempest provides a bridge distributed as tempest/blade
:
composer require tempest/blade
The next step is to provide the configuration needed for Blade to find your view files.
return new BladeConfig( viewPaths: [ __DIR__ . '/views/', ], );
Finally, update the view configuration to use the Blade renderer:
return new ViewConfig( rendererClass: \Tempest\View\Renderers\BladeViewRenderer::class, );
Using something else
Tempest refers to the view configuration to determine which view renderer should be used. By default, it uses Tempest View's renderer, Tempest\View\Renderers\TempestViewRenderer
. When using Blade or Twig, we provided Tempest\View\Renderers\BladeViewRenderer
or Tempest\View\Renderers\TwigViewRenderer
, respectively.
Implementing your own renderer
If you prefer using another templating engine, you will need to write your own renderer by implementing the Tempest\View\ViewRenderer
interface.
This interface only requires a render
method. It will be responsible for taking a Tempest\View\View
instance and rendering it to a PHP file.
As an example, the Blade renderer is as simple as the following:
use Tempest\Blade\Blade; use Tempest\View\View; use Tempest\View\ViewRenderer; final readonly class BladeViewRenderer implements ViewRenderer { public function __construct( private Blade $blade, ) { } public function render(View|string|null $view): string { return $this->blade->render($view->path, $view->data); } }
Once your renderer is implemented, you will need to configure Tempest to use it. This is done by creating or updating a ViewConfig
:
return new ViewConfig( rendererClass: YourOwnViewRenderer::class, );
Initializing your engine
The renderer will be called every time a view is rendered. If your engine has an initialization step, it may be a good idea to use a singleton initializer to construct it.
As an example, here is a simplified version of the initializer that creates the Blade
object, used by the Blade renderer:
use Tempest\Blade\Blade; use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; use Tempest\Container\Singleton; use Tempest\Reflection\ClassReflector; final readonly class BladeInitializer implements DynamicInitializer { public function canInitialize(ClassReflector $class): bool { return $class->getName() === Blade::class; } #[Singleton] public function initialize(ClassReflector $class, Container $container): object { $bladeConfig = $container->get(BladeConfig::class); return new Blade( viewPaths: $bladeConfig->viewPaths, ); } }