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:
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:
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:
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:
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:
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:
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:
$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:
<link rel="stylesheet" href="<?= $asset('css/app.css') ?>">
Latte and Twig receive a function:
<link rel="stylesheet" href="{asset('css/app.css')}">
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
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:
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:
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.