polidog / usephp-bear-module
BEAR.Sunday module that renders ResourceObjects via polidog/use-php's PSX (TSX-like) templates
Requires
- php: >=8.5
- bear/resource: ^1.20
- polidog/use-php: >=0.6.0 <0.8.0
- ray/di: ^2.16
Requires (Dev)
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-05-15 14:17:12 UTC
README
English | 日本語
Render BEAR.Resource ResourceObjects using polidog/use-php's PSX (TSX-like) templates.
A drop-in BEAR\Resource\RenderInterface adapter — your BEAR resources stay stateless and BEAR-idiomatic, but their HTML representations are authored as <div>{$count}</div> instead of nested H::div(children: [...]) calls.
Installation
composer require polidog/usephp-bear-module
PHP 8.5+. Requires bear/resource ^1.20 and polidog/use-php >=0.6.0 <0.8.0 (works unchanged on 0.6 and 0.7 — the deferred-rendering adapter is transparent to 0.7's additive Defer::$localCacheTtl).
Quick start
1. Author a PSX template per resource
Mirror your resource class structure under a templates root. For a resource at src/Resource/Page/Counter.php you place its template at templates/Page/Counter.psx:
<?php // templates/Page/Counter.psx declare(strict_types=1); use Polidog\UsePhp\Html\H; use Polidog\UsePhp\Runtime\Element; return function (array $props): Element { $count = (int) ($props['count'] ?? 0); return <div className="counter"> <h1>Counter</h1> <p>Count is {$count}</p> </div>; };
The template is a callable that takes the resource's $body (as array $props) and returns an Element.
2. Wire the module in your BEAR app
use Polidog\UsePhpBearModule\Module\UsePhpRendererModule; protected function configure(): void { // ... your other bindings ... $appMeta = $this->meta; // or however you obtain the AppMeta $this->install(new UsePhpRendererModule( templateDir: $appMeta->appDir . '/templates', cacheDir: $appMeta->tmpDir . '/psx', )); }
The module binds RenderInterface to UsePhpRenderer. Existing renderers (Twig etc.) get replaced for the whole app — only install where you want PSX as the default.
3. Resources stay BEAR-idiomatic
namespace MyApp\Resource\Page; use BEAR\Resource\ResourceObject; final class Counter extends ResourceObject { public function onGet(int $initial = 0): static { $this->body = ['count' => $initial]; return $this; } }
onGet populates $this->body. The renderer resolves the matching templates/Page/Counter.psx, compiles it on first use, and invokes it with $body as props.
Compile workflow
The renderer reuses the cache convention from polidog/use-php. Two modes:
# Production / CI: pre-compile templates as part of the build
./vendor/bin/usephp compile templates/ --cache=var/tmp/psx
./vendor/bin/usephp compile templates/ --cache=var/tmp/psx --check
// Dev: let the renderer compile on demand (default) new UsePhpRenderer( templateDir: __DIR__ . '/../templates', cacheDir: __DIR__ . '/../var/tmp/psx', autoCompile: true, // default; set to false for production );
vendor/bin/usephp compile and the renderer use the same hashing (sha1(realpath(template)).php), so a single pre-compile pass populates the cache the renderer reads from.
.gitignore:
**/var/cache/psx/
Overriding the template per resource
Convention is FQCN-based, but you can pin a specific template via the #[Template] attribute — same idea as BEAR's other declarative attributes (#[Embed], #[Link], #[Cacheable] …).
use Polidog\UsePhpBearModule\Annotation\Template; #[Template('shared/Counter.psx')] final class Counter extends ResourceObject { ... }
Resolution order:
- Custom
templateResolverclosure (when configured on the renderer or module — see "Conventions" below) #[Template]on the resource class- FQCN convention (
<templateDir>/<rest-after-Resource\>.psx)
Paths in the attribute are resolved relative to templateDir. Absolute paths are used as-is.
The attribute is class-level only. BEAR\Resource\RenderInterface::render($ro) doesn't tell the renderer which on* method was invoked, so a method-level attribute (e.g. one #[Template] on onGet and a different one on onPost) can't be resolved reliably. If you need different templates per HTTP verb, expose distinct resources.
Conventions
-
Template path =
<templateDir>/<everything-after-\Resource`-in-class-FQN>.psx. Example:MyApp\Resource\Page\Foo\Bar→/Page/Foo/Bar.psx. Override per-resource with#[Template]` (above). -
Custom resolution — pass a
templateResolverclosure toUsePhpRenderer(orUsePhpRendererModule) to bypass the default#[Template]+ FQCN logic entirely. The closure receives theResourceObjectand returns a path (relative totemplateDiror absolute):$this->install(new UsePhpRendererModule( templateDir: $appMeta->appDir . '/templates', cacheDir: $appMeta->tmpDir . '/psx', templateResolver: static function (\BEAR\Resource\ResourceObject $ro): string { // e.g. database lookup, manifest, format suffix, ... return $ro instanceof MyApp\Resource\Page\Counter ? 'shared/Counter.psx' : 'default.psx'; }, ));
When set, the resolver fully replaces both
#[Template]and the FQCN convention. Useful when you need a database-driven or context-aware mapping.UsePhpRendereritself isfinal— extension is via this hook, not subclassing. -
Props =
$ro->bodyif it's already an array;['body' => $ro->body]otherwise;[]if null. -
Return type = the template callable must return an
Elementor a string. Anything else throws. -
State / interactivity = stateless by default (Tier 1). Hooks + snapshot (
useState, form actions) are opt-in (Tier 3) — pass aUsePHPinstance to the renderer and useUsePhpActionResponderfromonPost. CDN-friendly partial hydration is available viaUsePhpDeferredResponder(see Deferred rendering).
Deferred rendering
usePHP ≥ 0.2 (stabilised through 0.7) supports deferred rendering —
CDN-friendly partial hydration. A per-user component (logged-in name, cart
count, A/B bucket) is split in two: the cacheable page renders only a
fallback, and the real component is fetched after load via a separate
GET /_defer/{name}. The page HTML stays user-independent and edge-cacheable;
only the small deferred fetch is per-user. See usePHP's docs for the template
side (fc(..., defer: new Defer(...)) / #[Defer]), the opt-in localStorage
client cache (Defer::$localCache, with an optional Defer::$localCacheTtl
time bound added in 0.7), and explicit reload (Defer::$reloadable). These
Defer knobs are page/component-side and rendered by usePHP into the
placeholder markup — this adapter is transparent to all of them, including
0.7's localCacheTtl.
The framework hook for the fetch is UsePHP::handleDeferred(). This package
wraps it in UsePhpDeferredResponder, mirroring UsePhpActionResponder:
use Polidog\UsePhpBearModule\UsePhpDeferredResponder; // A single resource catching the whole /_defer/... path: final class Defer extends ResourceObject { public function __construct(private UsePhpDeferredResponder $responder) {} public function onGet(): static { $html = $this->responder->handle($this); if ($html === null) { $this->code = 404; // not a defer route return $this; } $this->view = $html; return $this; } }
handle() builds the request from globals by default (pass a
Polidog\UsePhp\Router\RequestContext to override), copies usePHP's
per-endpoint Cache-Control onto $ro->headers, and maps an error status
(400/404/500) onto $ro->code — so the response travels through BEAR's
pipeline instead of raw header() / http_response_code() calls.
The deferred registry must be populated on the same UsePHP instance the
renderer was built with (the responder derives it from the renderer). Three
ways, in order of convenience:
loadComponentManifest()— auto-loads thedeferred-manifest.phpsidecar thatvendor/bin/usephp compilewrites for anyfc(..., defer: ...).registerDeferred($name, $fqcn, $cacheControl)— explicit.register(MyDeferredComponent::class)— for#[Defer]class components.
This responder requires the renderer to be in Tier 3 mode (constructed with a
UsePHP instance); it throws otherwise.
Tier in the BEAR + usePHP integration
- Tier 1 — stateless templates (default). PSX as a pure template engine:
no
useState, no hooks, no form actions. The plainUsePhpRenderer/UsePhpRendererModulepath. This is the BEAR-idiomatic baseline. - Tier 3 — hooks + snapshot (opt-in). Pass a
UsePHPinstance to the renderer sofc()/useStatetemplates serialise state into a signed snapshot, and useUsePhpActionResponderfromonPostto apply_usephp_actionsubmissions and return the updated fragment. - Deferred rendering (opt-in).
UsePhpDeferredResponderserves the/_defer/{name}endpoints described above — orthogonal to Tier 1/3, it only needs the renderer'sUsePHPinstance and a populated defer registry.
Tier 1 stays the default because hooks/actions collide with BEAR's resource-oriented model unless you opt in deliberately.
License
MIT