Run the same JSX components on a PHP backend. BarefootJS compiles your JSX into a marked template plus client JS; on the server, a small PHP runtime renders those templates. The runtime is deliberately template-engine- and web-framework-agnostic, so one implementation drives multiple stacks.
JSX → IR → marked template (.twig / .blade.php) + Component.client.js
│
▼
BarefootJS runtime ──delegates──▶ pluggable backend
(@barefootjs/php, `barefootjs/php`) (Twig | Laravel Blade)#Two backends, one runtime
| Backend | Template syntax | Compile-time package | Runtime | Where it runs |
|---|---|---|---|---|
| Twig | {{ }} / {% %} |
@barefootjs/twig |
Barefoot\TwigBackend |
any PHP web app (Slim, plain PHP) — no framework |
| Laravel Blade | {{ }} / @if / @foreach |
@barefootjs/blade |
Barefoot\BladeBackend |
illuminate/view standalone — no Laravel application/container required |
Both compile-time packages emit per-component template files and the shared
client JS, and both render through the same engine-agnostic PHP runtime
(Barefoot\BarefootJS, shipped by @barefootjs/php / Composer package
barefootjs/php). The only thing that differs is the backend: a tiny
object that implements the five operations the runtime delegates to.
Both adapters are near-mechanical ports of @barefootjs/jinja (the Jinja2
adapter — see the Python Adapter) to their respective
template syntax, handling the JS/PHP semantics divergences (truthiness,
stringification, reserved-word identifier mangling, and evaluator-only
higher-order-callback lowering, since neither Twig nor Blade has a lambda
expression) in one uniform place.
#The backend contract
Everything that depends on how a template renders — JSON marshalling, raw-string marking, JSX-children materialisation, named-template rendering, and template-variable-name mangling — lives behind a backend object with five methods:
| Method | Purpose |
|---|---|
encode_json($data) |
Serialize a value for bf-p props / inline JSON |
mark_raw($str) |
Mark already-safe HTML so the engine won't re-escape it |
materialize($value) |
Resolve captured JSX children to a string |
render_named($name, $bf, $vars) |
Render a child component's template |
ident($name) |
Mangle a prop name into an engine-safe template variable (Twig: grammar keywords like for → for_; Blade: render-scope collisions like loop → loop_) |
Because that is the only engine-specific surface, the runtime
(Barefoot\BarefootJS + Barefoot\Evaluator, in packages/adapter-php/src/)
is shared unchanged between TwigBackend and BladeBackend.
#Twig
npm install @barefootjs/twigConfigure the build (barefoot.config.ts):
import { createConfig } from '@barefootjs/twig/build'
export default createConfig({
components: ['./src/components'],
outDir: './dist',
})bf build emits .twig templates plus client JS under outDir. On the PHP
side, require barefootjs/twig via Composer and point a TwigBackend at the
emitted templates — it builds a FilesystemLoader-backed Twig\Environment
with the defaults the templates assume (autoescape: 'html',
strict_variables: false); pass a pre-built Environment via 'env' to
customize. Rendering goes through the shared runtime: construct a
Barefoot\BarefootJS over the backend and hand it to render_named:
use Barefoot\BarefootJS;
use Barefoot\TwigBackend;
$backend = new TwigBackend([
'paths' => ['dist/templates'],
]);
$bf = new BarefootJS(null, ['backend' => $backend]);
$html = $backend->render_named('user_card', $bf, ['name' => 'Ada']);Twig's default escaper emits "/' for "/', where the
Perl/Go/Python adapters emit the numeric "/' forms — the adapter
accounts for this byte-form difference so output stays consistent across
backends.
#Laravel Blade
npm install @barefootjs/bladeConfigure the build (barefoot.config.ts):
import { createConfig } from '@barefootjs/blade/build'
export default createConfig({
components: ['./src/components'],
outDir: './dist',
})bf build emits .blade.php templates plus client JS under outDir. Blade
runs on illuminate/view used standalone — no Laravel application or
service container required. Construct a Factory (Filesystem + an event
Dispatcher + an EngineResolver registering a blade engine over a
BladeCompiler + a FileViewFinder, all wired together by the Factory —
see Barefoot\BladeBackend's constructor — new BladeBackend(['paths' => …]) wires all of that for you) pointed at the emitted templates. Rendering
goes through the shared runtime, same as Twig:
use Barefoot\BarefootJS;
use Barefoot\BladeBackend;
$backend = new BladeBackend([
'paths' => ['dist/templates'],
]);
$bf = new BarefootJS(null, ['backend' => $backend]);
$html = $backend->render_named('user_card', $bf, ['name' => 'Ada']);Blade's {{ }} echo (Illuminate\Support\e()) emits named HTML entity forms
("/' via ENT_QUOTES) where Perl/Go/markupsafe emit the numeric
"/' forms — the adapter accounts for this byte-form difference so
output stays consistent across backends.
#Template output shape
Both backends share the same hydration-marker contract:
- One template file per component, named by snake-casing the PascalCase
component name (
UserCard→user_card.twig/user_card.blade.php). - Hydration markers use the same runtime method names as every other
adapter's
bf.*calls, spelled as PHP method calls on the$bfvariable (bf.scope_attr()/$bf->scope_attr(),bf.hydration_attrs()/$bf->hydration_attrs(),text_start/text_end,comment(...), …) — seespec/template-helpers.mdfor the shared helper contract. - Every text/attribute interpolation of a possibly-non-string value is routed
through
string(...)(orbool_str(...)for boolean-shaped values); every non-comparison condition position is routed throughtruthy(...); every JS===/!==comparison routes througheq(...)/neq(...)— PHP's own==/===are either loose or number-representation-sensitive in ways that diverge from JS strict equality.
#PHP runtime
packages/adapter-php (Composer package barefootjs/php, npm package
@barefootjs/php) is a self-contained, engine-agnostic PHP package with no
template-engine dependency. It implements the bf object every emitted
template calls into: hydration markers, context propagation
(provide_context/use_context), child-component rendering
(render_child), script registration, and the JS-compatible helper library
(string, bool_str, truthy, number, floor/ceil/round,
array/string helpers, spread_attrs, query, eq/neq, …). TwigBackend
(packages/adapter-twig/php/, Composer package barefootjs/twig) and
BladeBackend (packages/adapter-blade/php/, Composer package
barefootjs/blade) both depend on it via a composer path repository, so
adding a third PHP template engine means implementing the five-method backend
contract above, not re-porting the runtime.
#Examples
Runnable end-to-end apps that render the same shared components on a PHP
backend live under
integrations/php
(Twig) and
integrations/blade
(Blade).
#See also
- Python Adapter — the Jinja2 adapter this port maps from
- Adapter Architecture — the
TemplateAdapterinterface and IR contract - Writing a Custom Adapter