# Views
Kinetis deliberately has no template language of its own. There are already
excellent engines in the PHP ecosystem, so the framework chooses to **re-use,
not re-invent**. The view packages give controllers one small API while the
application selects pure PHP, Latte, or Twig at bootstrap.
## Choose one engine
Install the common contract and exactly one adapter:
```console
composer require kinetis/views kinetis/views-php
# or: composer require kinetis/views kinetis/views-latte
# or: composer require kinetis/views kinetis/views-twig
```
The adapter packages conflict with one another. Composer therefore refuses an
application that accidentally installs two engines and leaves its active one
ambiguous.
The conventional layout is:
```text
resources/
└── views/
└── articles/
└── index.php # or index.latte / index.twig
public/
├── css/app.css
└── js/app.js
```
`resources/views` is a convention, not hidden global state. Bootstrap supplies
an absolute root explicitly, so a project may put templates elsewhere without
depending on the process's current working directory. Static assets belong in
`public/`, where the web server can serve them without running PHP.
## Wire the application
Pure PHP:
```php
use Kinetis\Views\AssetUrl;
use Kinetis\Views\Views;
use Kinetis\ViewsPhp\PhpViewEngine;
$app->instance(
Views::class,
new Views(new PhpViewEngine(
__DIR__ . '/resources/views',
new AssetUrl('/'),
)),
);
```
Latte:
```php
use Kinetis\Views\AssetUrl;
use Kinetis\Views\ViewRuntime;
use Kinetis\Views\Views;
use Kinetis\ViewsLatte\LatteViewEngine;
$app->instance(
Views::class,
new Views(new LatteViewEngine(
__DIR__ . '/resources/views',
ViewRuntime::fromConfig(__DIR__, $config),
new AssetUrl('/'),
)),
);
```
Twig:
```php
use Kinetis\Views\AssetUrl;
use Kinetis\Views\ViewRuntime;
use Kinetis\Views\Views;
use Kinetis\ViewsTwig\TwigViewEngine;
$app->instance(
Views::class,
new Views(new TwigViewEngine(
__DIR__ . '/resources/views',
ViewRuntime::fromConfig(__DIR__, $config),
new AssetUrl('/'),
)),
);
```
`ViewRuntime::fromConfig()` follows Kinetis's `APP_ENV` rule. In development,
Latte and Twig compile from source without writing a disk cache. In production
(including an unset or unfamiliar `APP_ENV`), generated templates live beside
the AOT artifact in `.kinetis-cache/views/latte` or
`.kinetis-cache/views/twig`. The adapters own these directories; applications
do not configure vendor-specific cache paths or freshness flags.
This is worker-lifetime configuration: one immutable `Views` facade and one
engine are shared safely, while every call supplies a fresh data array. Do not
put request-specific values into a Twig global, a Latte extension, or an
application-scoped service used by a template.
## Render from a controller
Controllers depend only on the common package:
```php
use Kinetis\Views\Views;
use Psr\Http\Message\ResponseInterface;
final readonly class ArticleController
{
public function __construct(
private Views $views,
private ArticleRepository $articles,
) {}
public function index(): ResponseInterface
{
return $this->views->response('articles/index', [
'articles' => $this->articles->latest(),
]);
}
}
```
The logical name is extensionless and relative. The selected adapter adds
`.php`, `.latte`, or `.twig`. Absolute names, traversal, empty path segments,
and names that already carry an engine extension are rejected. A missing file
throws `ViewNotFoundException`; a template failure throws
`ViewRenderException` with the engine's original exception as its cause.
Use `render()` when a string is needed instead of an HTTP response:
```php
$html = $this->views->render('mail/welcome', ['user' => $user]);
```
## The one helper: `asset()`
Kinetis does not inject a global bag of URL, logger, container, or request
helpers. Hidden service access is particularly unsafe in persistent workers,
where an application-scoped object can accidentally retain request state.
The initial view contract includes only `asset()`: a deterministic URL join
with no I/O and no application state.
Pure PHP receives an invokable `$asset` variable:
```php
```
Latte and Twig receive a function:
```html
```
```twig
```
With the default `new AssetUrl('/')`, all three produce `/css/app.css`.
A root-relative prefix such as `/static` or an HTTPS CDN base is also accepted.
Asset paths stay relative and cannot contain traversal, a query, or a fragment.
The helper does not check the filesystem, fingerprint files, or read a build
manifest.
Data keys use template-variable names. `asset`, `GLOBALS`, and `this` are
reserved. This keeps the same data contract across all three engines and
prevents one engine from shadowing a helper or PHP runtime variable even if
another engine would happen to resolve the collision differently.
## Escaping and engine customization
Latte and Twig auto-escape ordinary HTML output. Pure PHP deliberately follows
PHP's own rules: escape untrusted text with `htmlspecialchars()` and use
context-appropriate encoding for attributes, URLs, JavaScript, and JSON.
The adapter does not guess the output context.
For engine-specific filters or extensions, configure the adapter during
bootstrap through `LatteViewEngine::engine()` or
`TwigViewEngine::engine()`. Keep those choices out of controllers so changing
the adapter does not change controller code. Twig constructor options remain
available for engine behavior such as `strict_variables`; `cache` and
`auto_reload` are reserved for `ViewRuntime`.
## Warm and clear the cache
The common package contributes two commands for whichever adapter the
application bound:
```console
php vendor/bin/kinetis views:warm
php vendor/bin/kinetis views:clear
```
`views:warm` empties the selected adapter's cache, recursively scans the whole
configured view root for `.latte` or `.twig` files, and compiles them in stable
logical-name order. It does not follow symbolic links. Clearing first is
intentional: with production freshness checks disabled, asking either vendor to
load an existing compiled file could preserve a template from the previous
deployment. A compile error fails the command instead of reporting a partial
cache as ready.
Pure PHP has no generated view cache, so both commands succeed with a zero
count. In development, Latte and Twig warming is likewise a successful zero-
work operation; `views:clear` still removes production artifacts left in the
project cache directory.
Both commands run the normal package and application bootstrap so they compile
the exact configured engine, functions, extensions, and root that workers use.
Run them with the application's complete deployment environment. Bootstrap
registrations should remain lazy: binding a connection factory is appropriate,
opening a database connection merely because bootstrap ran is not.
In production, build both cache layers before starting or restarting workers:
```console
php vendor/bin/kinetis build
php vendor/bin/kinetis views:warm
```
After first installing or updating `kinetis/views`, run `kinetis build` so the
production AOT command registry includes the package-provided view commands.
## Local files and non-blocking I/O
Template loading and compilation use local filesystem I/O in all three engines.
That work is not an asynchronous network boundary and cannot be made
Revolt-aware by the view facade. In production, deploy templates with the
application, keep them on local storage, enable OPcache, and make
`.kinetis-cache/` writable to the deploy command.
In a persistent worker, Latte and Twig load a compiled template class once per
process; neither engine performs a freshness check after that class is loaded,
even when disk caching is disabled. Development therefore provides source
loading, not in-process class replacement: a persistent-runtime development
supervisor must restart workers when a template changes. A boot-per-request
runtime such as ordinary PHP-FPM sees the new source on the next request. A
production deployment must warm the new cache and restart workers; a CLI cache
operation cannot unload classes from a separate serving process.
Do not render templates from network filesystems or fetch templates during a
request. Remote data belongs in non-blocking application services and should be
fully resolved before `render()` is called. Rendering is buffered: it returns
one complete string/response, not a streaming response, and the pure-PHP adapter
restores output-buffer depth if a template throws.