tihloh / prefab-live
Server-driven reactive PHP components for Prefab PHP with signed state, explicit actions and a tiny browser runtime.
Requires
- php: >=8.1
- tihloh/prefab-core: ^0.4 || ^0.5
Requires (Dev)
None
Suggests
- tihloh/prefab-input: Optional validation and normalization for component actions.
- tihloh/prefab-routes: Optional routing integration for the Prefab Live endpoint.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-20 15:13:15 UTC
README
Prefab Live adds server-driven reactive PHP components to Prefab without requiring a JavaScript framework.
Keep application state and actions in PHP. Use small
pf:*attributes to connect the browser to the server.
Prefab Live v0.1 focuses on the core reactive loop:
- PHP component classes;
- public component state;
pf:modelstate binding;pf:clickactions;pf:submitform actions;pf:loadingrequest state;- signed snapshots;
- explicit
#[Action]methods; #[Locked]state that may be rendered but not client-updated;- optional CSRF validation;
- component-local HTML replacement;
- lifecycle hooks:
mount(),hydrate(),dehydrate(); - lightweight error bags on components;
- a small framework-independent browser runtime.
Prefab Live does not require Laravel, Livewire, React, Vue, Alpine or jQuery.
Requirements
- PHP 8.1 or newer
- Composer
- a route/endpoint capable of receiving JSON POST requests
Installation
When published:
composer require tihloh/prefab-live
1. Create a component
use Tihloh\Prefab\Live\Attributes\Action; use Tihloh\Prefab\Live\Attributes\Locked; use Tihloh\Prefab\Live\Component; final class Counter extends Component { public int $count = 0; #[Locked] public int $ownerId = 0; protected function mount(int $start = 0): void { $this->count = $start; $this->ownerId = 42; } #[Action] public function increment(): void { $this->count++; } public function render(): string { return <<<HTML <button pf:click="increment">Count: {$this->count}</button> <span pf:loading>Working...</span> HTML; } }
Only methods marked with #[Action] may be called by browser requests.
2. Register components
The browser sends a component alias, never a PHP class name. The server resolves the alias through an explicit registry:
use Tihloh\Prefab\Live\ComponentRegistry; $registry = new ComponentRegistry(); $registry->register('counter', Counter::class);
Factories are also supported for dependency injection:
$registry->register('users.search', fn () => new UserSearch($users));
3. Create the Live manager
Use a private application signing key of at least 32 bytes:
use Tihloh\Prefab\Live\LiveManager; $live = new LiveManager( registry: $registry, signingKey: $_ENV['PREFAB_LIVE_KEY'], endpoint: '/prefab/live', );
Do not expose PREFAB_LIVE_KEY to the browser or commit it to source control.
4. Mount a component
<?= $live->mount('counter', ['start' => 5]) ?>
Prefab Live wraps the rendered component with its alias, instance ID, signed snapshot, endpoint and optional CSRF token.
5. Add the browser runtime
Serve assets/prefab-live.js from your application's public assets and load it once:
<script src="/assets/prefab-live.js" defer></script>
No build step is required.
6. Handle the Live endpoint
The endpoint receives JSON and returns the result of handle() as JSON.
Plain PHP example:
$payload = LiveManager::decodeRequest(file_get_contents('php://input') ?: ''); $response = $live->handle( $payload, $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null, ); header('Content-Type: application/json'); echo json_encode($response, JSON_THROW_ON_ERROR);
Prefab Live itself does not call exit() or force a response abstraction. Your application or router owns HTTP output and error handling.
7. pf:model
Bind normal form controls to public component properties:
final class ProfileForm extends Component { public string $name = ''; public bool $active = false; #[Action] public function save(): void { // Persist $this->name and $this->active. } public function render(): string { $name = htmlspecialchars($this->name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $checked = $this->active ? 'checked' : ''; return <<<HTML <form pf:submit="save"> <input name="name" pf:model="name" value="{$name}"> <label> <input type="checkbox" pf:model="active" {$checked}> Active </label> <button type="submit">Save</button> <span pf:loading hidden>Saving...</span> </form> HTML; } }
In v0.1, pf:model values are collected when a Live action is sent. Automatic live/debounced model requests are intentionally deferred to a later version.
Nested array paths are supported:
<input pf:model="filters.search"> <input pf:model="address.city">
The top-level public property must be an array for nested updates.
8. Forms
<form pf:submit="save"> <input pf:model="name"> <input pf:model="email"> <button type="submit">Save</button> <span pf:loading>Saving...</span> </form>
The browser runtime prevents the normal form submission, sends current model values plus the action, then replaces only that component's rendered HTML.
9. Loading UI
Any element with pf:loading is hidden when idle and visible while the component request is running:
<button pf:click="refresh">Refresh</button> <span pf:loading>Loading...</span>
10. Lifecycle
A component may define:
protected function mount(int $userId): void { // Initial mount only. } protected function hydrate(): void { // After signed state is restored on a Live request. } protected function dehydrate(): void { // Final cleanup hook after rendering/snapshot creation. }
Lifecycle methods are infrastructure hooks, not browser-callable actions.
11. Component errors
Components have a small error bag for render-time validation feedback:
#[Action] public function save(): void { $this->clearErrors(); if (trim($this->name) === '') { $this->addError('name', 'Name is required.'); return; } }
Then in render():
$error = $this->error('name');
Prefab Input can later be used inside actions for richer validation without making it mandatory for Prefab Live.
12. Security model
A Live request contains:
component alias
instance ID
signed previous snapshot
model updates
optional action
The server performs this sequence:
request
↓
verify signed snapshot
↓
resolve alias through ComponentRegistry
↓
restore public state
↓
apply explicit model updates
↓
call only a #[Action] method
↓
render
↓
sign next snapshot
↓
response
Important rules:
- Browser requests never choose arbitrary PHP classes.
- Public methods are not remotely callable unless they have
#[Action]. - Previous state is protected by an HMAC checksum.
- Mark identifiers or server-controlled public state with
#[Locked]when the browser must not update them. - Public component state is browser-visible. Do not put passwords, API secrets, access tokens or other secrets in public properties.
- Authorization still belongs in your application/action. A signed request does not replace permission checks.
- Use HTTPS in production.
- Use your application's CSRF validation for authenticated browser sessions.
13. CSRF integration
Pass the rendered CSRF token and a validator:
$live = new LiveManager( registry: $registry, signingKey: $_ENV['PREFAB_LIVE_KEY'], endpoint: '/prefab/live', csrfToken: $session->csrfToken(), csrfValidator: fn (?string $token): bool => $session->validateCsrf($token), );
The browser runtime sends the token in X-CSRF-Token.
14. State types
Prefab Live v0.1 public state is intentionally simple:
null
string
int
float
bool
array of JSON-safe values
Objects, database connections, service objects and resources belong in private/protected properties or constructor-injected dependencies, not serialized public state.
Typed public scalar properties are safely coerced from browser form values where possible.
15. Request protocol
Example request:
{
"id": "f12ab34cd56ef789",
"component": "counter",
"snapshot": {"count": 5},
"checksum": "...",
"updates": {"count": "7"},
"action": {"method": "increment", "params": []}
}
Example response:
{
"id": "f12ab34cd56ef789",
"component": "counter",
"html": "<button pf:click=\"increment\">Count: 8</button>",
"snapshot": {"count": 8},
"checksum": "..."
}
16. v0.1 responsibility boundary
Prefab Live owns:
PHP component state
↕
signed Live protocol
↕
tiny browser bridge
↕
component-local DOM replacement
It does not own:
- your database/business model;
- application authorization policy;
- page routing;
- a template engine;
- frontend styling;
- general JavaScript application state;
- file uploads yet;
- nested Live components yet;
- polling/lazy loading yet;
- URL/query-string binding yet;
- automatic/debounced live model syncing yet.
Those features can be added after the base protocol is stable.
17. Design principle
Prefab Live follows the wider Prefab rule:
Prefab automates reusable plumbing. Your application keeps control of behavior and architecture.
For ordinary interactive PHP screens, the target experience is:
normal PHP
+
normal HTML
+
a few pf:* attributes
=
server-driven reactive UI