preflow / twig
Preflow Twig adapter — Twig template engine implementation
Requires
- php: >=8.4
- preflow/components: ^0.1 || @dev
- preflow/core: ^0.60 || @dev
- preflow/view: ^0.72 || @dev
- twig/twig: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.0
- preflow/testing: ^0.64 || @dev
Suggests
- preflow/htmx: Required for HdExtension (hd.post, hd.get helpers)
Provides
None
Conflicts
- preflow/core: <0.60
- preflow/view: <0.72
Replaces
None
- dev-main
- v0.76.0
- v0.75.0
- v0.74.0
- v0.73.0
- v0.72.1
- v0.72.0
- v0.71.1
- v0.71.0
- v0.70.0
- v0.69.2
- v0.69.1
- v0.69.0
- v0.68.1
- v0.68.0
- v0.67.0
- v0.66.2
- v0.66.1
- v0.66.0
- v0.65.0
- v0.64.0
- v0.63.2
- v0.63.1
- v0.63.0
- v0.62.0
- v0.61.0
- v0.60.1
- v0.60.0
- v0.59.0
- v0.58.1
- v0.58.0
- v0.57.0
- v0.56.1
- v0.56.0
- v0.55.0
- v0.54.1
- v0.54.0
- v0.53.2
- v0.53.1
- v0.53.0
- v0.52.0
- v0.51.0
- v0.50.0
- v0.49.1
- v0.49.0
- v0.48.5
- v0.48.4
- v0.48.3
- v0.48.2
- v0.48.1
- v0.48.0
- v0.47.1
- v0.47.0
- v0.46.1
- v0.46.0
- v0.45.0
- v0.44.0
- v0.43.0
- v0.42.1
- v0.42.0
- v0.41.2
- v0.41.1
- v0.41.0
- v0.40.0
- v0.39.0
- v0.38.0
- v0.37.0
- v0.36.0
- v0.35.0
- v0.34.1
- v0.34.0
- v0.33.0
- v0.32.0
- v0.31.0
- v0.30.0
- v0.29.0
- v0.28.0
- v0.27.1
- v0.27.0
- v0.26.0
- v0.25.1
- v0.25.0
- v0.24.1
- v0.24.0
- v0.23.0
- v0.22.1
- v0.22.0
- v0.21.0
- v0.20.0
- v0.19.2
- v0.19.1
- v0.19.0
- v0.18.1
- v0.18.0
- v0.17.2
- v0.17.1
- v0.17.0
- v0.16.2
- v0.16.1
- v0.16.0
- v0.15.0
- v0.14.2
- v0.14.1
- v0.14.0
- v0.13.1
- v0.13.0
- v0.12.0
- v0.11.4
- v0.11.3
- v0.11.2
- v0.11.1
- v0.11.0
- v0.10.4
- v0.10.3
- v0.10.2
- v0.10.1
- v0.10.0
- v0.9.1
- v0.9.0
- v0.8.0
- v0.7.0
- v0.6.0
This package is auto-updated.
Last update: 2026-11-14 19:29:52 UTC
README
Twig 3 adapter for Preflow. Implements TemplateEngineInterface and ships co-located CSS/JS support via {% apply css %} / {% apply js %} filters and {{ head() }} / {{ assets() }} functions.
Installation
composer require preflow/twig
Requires PHP 8.4+ and Twig 3.
What's included
| Component | Description |
|---|---|
TwigEngine |
TemplateEngineInterface implementation wrapping Twig\Environment |
PreflowExtension |
{% apply css %}, {% apply js %}, {{ head() }}, {{ assets() }} |
ComponentExtension |
{{ component('Name', {props}) }} — renders Preflow components |
HdExtension |
{{ hd.post(...) }}, {{ hd.get(...) }} — hypermedia action helpers |
{{ t() }} and {{ tc() }} come from preflow/i18n (TranslationExtensionProvider, registered by the application), which escapes parameters.
TwigEngine
use Preflow\Twig\TwigEngine; use Preflow\View\AssetCollector; use Preflow\View\NonceGenerator; $assets = new AssetCollector(new NonceGenerator(), isProd: true); $engine = new TwigEngine( templateDirs: [__DIR__ . '/templates', __DIR__ . '/app/pages'], assetCollector: $assets, debug: false, cachePath: __DIR__ . '/storage/twig-cache', // null = no cache ); $html = $engine->render('blog/post.twig', ['post' => $post]); $engine->exists('partials/nav.twig'); // bool $engine->getTemplateExtension(); // 'twig'
Co-located styles and scripts
Use {% apply css %} and {% apply js %} anywhere in a template. The content is registered with the AssetCollector and nothing is output at that point.
{# templates/blog/post.twig #} {% apply css %} .post-title { font-size: 2rem; font-weight: 700; } .post-body { line-height: 1.7; } {% endapply %} {% apply js %} document.querySelector('.post-body a[href^="http"]') ?.setAttribute('target', '_blank'); {% endapply %} {% apply js('head') %} window.analyticsId = {{ post.id }}; {% endapply %} <h1 class="post-title">{{ post.title }}</h1> <div class="post-body">{{ post.body|raw }}</div>
JS position argument: 'body' (default), 'head', or 'inline'.
Layout with head() and assets()
{{ head() }} renders the collected CSS and the JS registered for the <head>. {{ assets() }} renders body JS, then inline JS — place it just before </body>.
{# templates/_layout.twig #} <!doctype html> <html> <head> <meta charset="utf-8"> <title>{% block title %}App{% endblock %}</title> {{ head() }} </head> <body> {% block content %}{% endblock %} {{ assets() }} </body> </html>
asset_url()
The asset_url() template function generates a URL to a public asset. Register it via your AppServiceProvider to point at the correct public path.
<link rel="stylesheet" href="{{ asset_url('css/app.css') }}"> <img src="{{ asset_url('images/logo.png') }}" alt="Logo">
Register in your service provider:
$engine->addFunction(new TemplateFunctionDefinition( name: 'asset_url', callable: fn (string $path) => '/public/' . ltrim($path, '/'), isSafe: false, ));
Live parts: {% live %}
A part of a page that differs per visitor (a guest's album count, "in your album", a cart count) is a live part of a cached page (preflow/core's page cache):
{% live with {number: object.number} %}
<button aria-pressed="{{ in_album(number) ? 'true' : 'false' }}">…</button>
{% endlive %}
- The body becomes a block of the template (
__live_{line}) and sees only thewithvalues and the globals, never the variables around it.withtakes scalars and arrays; it may be left out ({% live %}). - While a page renders for the page cache, the tag prints a signed placeholder (template, block, values) instead of the body; the block is rendered for each visitor when the page is served. Otherwise it renders in place.
- Live blocks don't nest. Not inside
{% embed %}. Block names starting with__live_are theirs: a template's own{% block __live_… %}is a syntax error. - The placeholder names the template by its loader name (
page.twig,@folio/...), or, in a component's own template, by the component class. A template rendered by a path that is no component's renders its live blocks in place and keeps the page from being stored. - Print the tag's output as it is: captured into
{% set %}and printed escaped, or passed through|striptagsor|slice, the placeholder is broken and the part is missing.
Freshness and expiry: {% freshness %}, {% expires %}
The page cache's dial for what a part of a page reads (preflow/core's page cache, "Freshness, per dependency"), and the page's expiry:
{% freshness 'next-visit' %}{% for other in jobs_of(job.company) %}…{% endfor %}{% endfreshness %}
{% freshness 'timed' every '12h' %}{{ weather() }}{% endfreshness %}
{% expires at event.startsAt %}
{% expires in '1h' %}
- Names:
'fresh','next-visit'(or'next_visit'),'timed'; onlytimedtakesevery('30m','12h','2d'or seconds). Both are expressions. A timed page never outlives the page cache'smax_age(4 h by default):every '12h'refreshes every 4 h unless it is raised (up to 20 h). - Nested, the innermost wins. The body sees and sets the variables around it.
expires attakes aDateTimeInterface, a unix time or a date string (null does nothing); the earliest of several counts.- Without the page cache the body renders as it is; a wrong name or duration fails either way.
Inside
{% live %}it does nothing.
Engine configuration
Set APP_ENGINE=twig in your .env (this is the default). Preflow's Application will automatically create a TwigEngine and register all extension providers.