A reactive controller-rendering protocol your app uses to update parts of a server-rendered page without a full reload. Your controllers stay the source of truth; ForgeWire handles the targeted updates between them and the browser.
ForgeWire is a protocol: a contract between your server-rendered controllers and a small client-side runtime. You mark a controller as reactive, annotate its state and actions, and mark the regions of the view you want to update. When the visitor interacts, the browser sends a wire request, your controller runs, and ForgeWire patches just those marked regions back into the page.
It is not a component framework that re-renders whole pages or swaps component lifecycles in the browser. There are no frontend-mounted components in the Vue/React sense — your PHP controller remains the single place logic lives, and the browser is a thin client following a well-defined protocol.
Prerequisite: ForgeWire builds on the routing layer (wire requests are routed HTTP requests), so it needs the router capability present in your app.
/__wire with an
X-ForgeWire header) carrying the
action, parameters, and a signed snapshot of
the controller state.
Because state round-trips through your session and is signed, the server stays authoritative; the client is never asked to "own" application state.
php forge.php package:install-module --module=ForgeWire
Installing links the client assets (the
forgewire.js runtime) so they're
served to your pages, and wires up the middleware
that handles wire requests. The runtime is injected
into your responses automatically.
A reactive controller is a normal controller
marked with the Reactive attribute.
Compose the helper trait to gain the response
verbs (events, flashes, redirects):
use Modules\ForgeWire\Attributes\Reactive;
use Modules\ForgeWire\Traits\ReactiveEndpointHelper;
#[Reactive]
final class CounterController
{
use ReactiveEndpointHelper;
#[State]
public int $count = 0;
#[Action]
public function increment(): void
{
$this->count++;
}
}
The controller is still a controller — it renders a view like any other. ForgeWire only adds the reactive lifecycle: restore state, run the action, re-render the marked regions.
Three annotations shape the controller's reactive surface:
#[State] — a property that
round-trips with the client. State is
hydrated from your session on each request
(a shared flag makes a value
shared across the component's islands).
#[Computed] — a method whose
result is memoized for the request; handy
for derived values you use in the view.
#[Action] — a method callable
from the client, with arguments supplied by
the browser. Actions are the only entry
points the frontend may invoke.
use Modules\ForgeWire\Attributes\State;
use Modules\ForgeWire\Attributes\Computed;
use Modules\ForgeWire\Attributes\Action;
#[Reactive]
final class CartController
{
use ReactiveEndpointHelper;
#[State]
public array $items = [];
#[Computed]
public function total(): float
{
return array_sum(array_column($this->items, 'price'));
}
#[Action]
public function add(string $sku, int $qty = 1): void
{
$this->items[] = ['sku' => $sku, 'price' => price($sku), 'qty' => $qty];
}
}
Because only #[Action] methods are
callable from the browser, your controller's other
logic can't be invoked directly — one of the ways
the surface stays small and safe.
Validate state properties with the
Validate attribute — rules as an
array or a pipe-separated string, with custom
messages. Invalid input surfaces as a validation
failure instead of an action running on bad data:
use Modules\ForgeWire\Attributes\State;
use Modules\ForgeWire\Attributes\Validate;
#[Reactive]
final class SignupController
{
use ReactiveEndpointHelper;
#[State]
#[Validate(rules: ['required', 'email'], messages: ['email' => 'Enter a valid email.'])]
public string $email = '';
#[State]
#[Validate(rules: 'required|min:3')]
public string $name = '';
}
Reactivity happens inside explicit islands.
Mark a region with the fw_id() helper.
Without a marked island there is no reactivity —
the boundary is required. A page can hold several
independent islands, each with its own reactive
state.
// counter view
<div <?= fw_id('main-counter') ?> class="counter-box">
<h1>Count: <?= $count ?></h1>
<button fw:click="increment">Add One</button>
</div>
<!-- a second, independent island on the same page -->
<div <?= fw_id('info-panel') ?> class="info-box">
<p fw:poll.5s>Updated at: <?= date('H:i:s') ?></p>
</div>
Interactions are wired with element directives —
fw:click, fw:submit,
fw:model (two-way-ish field binding),
fw:param (action arguments),
fw:depends, and fw:event.
The client observes these and issues wire requests.
Cleaning up unused islands keeps memory bounded; an automatic cleanup service and a CLI tool handle stale components.
Inside an action, tell the client to do more than patch HTML — queue a redirect, show a flash, or emit an event other islands (or other page code) can listen for:
#[Action]
public function save(): void
{
$this->account->save();
$this->flash('success', 'Saved!');
$this->dispatch('account.updated', ['id' => $this->account->id]);
$this->redirect('/dashboard');
}
These come from the WithWireResponse
trait composed through
ReactiveEndpointHelper. Redirects can
carry a small delay; flashes and events are
delivered back in the wire response.
For live-ish content, an island can poll the server
on an interval using the fw:poll
directive with a duration suffix:
<div <?= fw_id('scores') ?>>
<ul fw:poll.10s>
<?php foreach ($scores as $s): ?>
<li><?= $s ?></li>
<?php endforeach; ?>
</ul>
</div>
Polling is smart about visibility: the runtime pauses polling while an island is off-screen and resumes when it's visible again.
The protocol is built around server authority, and several guards are on by default:
#{Action} methods are callable
from the client.
Behavior is tuned with a couple of environment variables, both with sensible defaults:
# Serve the minified client runtime (recommended in production)
FORGE_WIRE_USE_MINIFIED=true
# How long an idle reactive component is kept before cleanup (seconds)
FORGEWIRE_COMPONENT_TTL_SECONDS=1800
Use FORGE_WIRE_USE_MINIFIED=false
during development to keep the readable runtime for
debugging.
# Remove stale reactive components
php forge.php modules:forgewire:cleanup
# Regenerate the minified client runtime
php forge.php modules:forgewire:minify
The cleanup command prunes components whose session state is stale, and the minify command regenerates the compressed client script after you change the runtime sources.