Lightweight Livewire-like reactive components for Forge Kernel. Build dynamic interfaces with server-side interactivity, security by default, and support for complex data types.
ForgeWire provides Livewire-like reactivity for standard Controllers. It enables server-side interactivity without WebSockets, JavaScript frameworks, or complex client-side state management. Unlike other frameworks, you don't need to create separate "component classes" — any controller can become reactive with simple attributes.
ForgeWire transforms the standard request-response cycle into a persistent, reactive loop. It achieves this by intercepting requests and re-rendering specific view fragments based on the updated state of your controller.
When a controller is marked with #[Reactive], ForgeWire manages its
lifecycle:
Creating a reactive feature requires three steps: mark the controller, mark the state, and wrap the view with island identifiers.
#[Middleware("web")]
#[Reactive]
final class CounterController
{
use ControllerHelper;
#[State]
public int $count = 0;
#[Route("/counter")]
public function index(): Response
{
return $this->view("pages/counter", ['count' => $this->count]);
}
#[Action]
public function increment(): void
{
$this->count++;
}
}
Use fw_id() helper to create reactive islands. Without this helper, there is no reactivity - the island boundary is required for ForgeWire to work. You can have multiple islands per page, each with its own reactive state.
<!-- resources/views/pages/counter.php -->
<div <?= fw_id('main-counter') ?> class="counter-box">
<h1>Count: <?= $count ?></h1>
<button fw:click="increment">Add One</button>
<div fw:target>Result: <?= $count ?></div>
</div>
<!-- Another island on same page -->
<div <?= fw_id('info-panel') ?> class="info-box">
<p fw:poll.5s>Updated at: <?= date('H:i:s') ?></p>
</div>
fw_id() creates the reactive boundary - no reactivity without itfw:target for efficient partial updates#[State(shared: true)] with fw:dependsForgeWire attributes function as permissions and behavior modifiers, not data exposure. No data is automatically exposed to views - you explicitly pass data in your controller methods.
Applied to the class. Permission gate - enables ForgeWire to monitor this controller. Controllers without this attribute are completely ignored by the reactive engine for security.
Applied to properties. Permission to persist - marks properties that should be tracked between requests. The shared: true option allows multiple islands to access the same state value.
Applied to methods. Permission to call - marks the method as callable from browser. Without this attribute, methods cannot be invoked from frontend. The optional submit parameter indicates this action handles form submissions.
Applied to properties. Validation rules - defines validation rules and custom error messages for properties that receive user input.
Applied to public properties. Permission to persist - marks properties that should be tracked between requests. Supports scalars (int, string, bool), arrays, and objects. The shared: true option allows multiple islands to access the same state value.
Use #[State(shared: true)] to share state across multiple islands on the same page. Islands must opt-in to shared dependencies using fw:depends to receive updates when shared state changes.
⚠️ Important: Islands must opt-in to shared state dependencies using fw:depends. Without this directive, islands will not receive shared state updates even if marked with #[State(shared: true)].
// Controller
#[State(shared: true)]
public int $counter = 0;
#[State(shared: true)]
public array $jobs = [];
<!-- View - Islands opt-in to specific shared state properties -->
<div <?= fw_id('todo-app') ?> fw:depends="counter">
<h1>Reactive Todo List counter: <?= $counter ?></h1>
<!-- Content that uses $counter -->
</div>
<div <?= fw_id('counter-app') ?> fw:depends="counter">
<h1>Counter</h1>
<button fw:click="increment">Increment</button>
<div fw:target><?= $counter ?></div>
</div>
<!-- Multiple dependencies separated by comma -->
<div <?= fw_id('queue-stats') ?> fw:depends="jobs,stats">
<h2>Statistics</h2>
<p fw:target>Total Jobs: <?= count($jobs) ?></p>
</div>
#[State(shared: true) to properties that should be sharedfw:depends="prop1,prop2" to specify which shared properties they needUse Cases: Perfect for shopping carts, user notifications, queue dashboards, or any data that needs to stay synchronized across different components on same page.
Use Case: Perfect for shopping carts, user notifications, or any data that needs to stay synchronized across different components on the same page.
ForgeWire uses HTML attributes (directives) to bind your UI to your controller.
fw:model="name" - Immediate binding
(updates on every keystroke)fw:model.debounce="name" - Updates
after 600ms of inactivityfw:model.defer="name" - Updates only
when an action is triggeredfw:click="save" - Call action on
clickfw:submit="create" - Call action on
form submit (auto-prevents refresh)fw:keydown.enter="search" - Call
action on Enter key
To pass data to an action, use the fw:param- prefix.
<!-- Controller: public function delete(int $id) -->
<button fw:click="delete" fw:param-id="123">Delete Post</button>
ForgeWire operates on a "deny-by-default" principle:
APP_KEY. If a user modifies the
state in the console, the server rejects the request.#[Action] methods can be invoked. Even
protected/private methods are strictly blocked.Applied to methods. Marks the method as "safe" to be called from the browser. ForgeWire will refuse to call any method not explicitly marked as an action.
The input() action is built-in and
handles
fw:model updates:
// Automatically called for fw:model
#[Action]
public function input(...$keys): void
{
// Called when fw:model values change
// $keys contains which properties changed
}
<!-- fw:click -->
<button fw:click="increment">+</button>
<button fw:click="update" fw:param-id="5" fw:param-name="New Name">Update</button>
<!-- fw:submit -->
<form fw:submit="save">
<input fw:model="name" type="text">
<button type="submit">Save</button>
</form>
Add validation rules to properties using #[Validate] attribute. Validation supports both array and JSON message formats for backward compatibility.
#[State]
#[Validate('required|min:3', messages: ['required' => 'Name is required', 'min' => 'Name must be at least :value characters'])]
public string $formName = '';
#[State]
#[Validate('required|email', messages: ['required' => 'Email is required', 'email' => 'Please enter a valid email address'])]
public string $formEmail = '';
:value placeholders in messages#[Action(submit: true)] for form handlingValidation runs automatically when dirty state includes the property.
ForgeWire's JavaScript client handles all frontend interactions automatically.
fw:click="save" — Call action on
click (use fw:param-* for arguments)fw:submit="save" — Form
submission (auto-prevents refresh)fw:keydown.enter="submit" — Call action on Enter keyfw:keydown.escape="cancel" — Call action on Escape keyfw:model="property" — Two-way
binding (immediate)fw:model.lazy — Update on
blur/changefw:model.debounce — Debounced
updates (600ms default)fw:model.debounce.300ms — Custom
debounce timefw:poll — Auto-refresh every 2
secondsfw:poll.5s — Custom poll intervalfw:poll.3s fw:action="onPoll" — Custom action on pollfw:id("name") — PHP helper to create unique island identifierfw:target — Marks element for partial updates (performance optimization)fw:loading — Shows content during action processingfw:depends="prop1,prop2" — Opt-in to shared state dependencies (comma-separated)fw:validation-error="fieldName" — Display validation errors
⚠️ Deprecated: fw:shared is deprecated and should not be used. Use fw:depends to opt-in to shared state dependencies.
fw:param-id="123" — Pass parameters to actionsfw:param-name="value" — String parametersfw:param-count="5" — Numeric parametersfw:id("name") — PHP helper to create unique island identifierfw:target — Marks element for partial updates (performance optimization)fw:loading — Shows content during action processing<!-- Multiple islands on same page -->
<div <?= fw_id('counter-1') ?>>
<button fw:click="increment">+1</button>
<div fw:target>Count: <?= $count ?></div>
</div>
<div <?= fw_id('search-box') ?>>
<input fw:model.debounce="query" placeholder="Search...">
<div fw:loading>Searching...</div>
<div fw:target><?= $results ?></div>
</div>
<div fw:id="fw-0b4c5e3d9e83" >
<div class="counter">
<button fw:click="decrement">–</button>
<span>2 (even)</span>
<button fw:click="increment">+</button>
</div>
</div>
Components only poll when visible on screen (performance optimization):
<div fw:id="..." fw:poll.5s>
<!-- Only polls when visible -->
</div>
ForgeWire implements a reactive protocol for PHP that enables server-side interactivity without WebSockets or complex client-side state management. It's built specifically for Forge's architecture and follows PHP conventions.
Attributes work as permissions, not data exposure
Reactivity only works with explicit island boundaries
# Generate new ForgeWire island
forgewire:island --type=app --name=interactive-counter --kind=component
# Generate for module
forgewire:island --type=module --module=ForgeEvents --name=queue-dashboard --kind=page
# Interactive wizard
forgewire:island
Use this trait for enhanced reactive functionality:
use ReactiveControllerHelper;
// Available methods
public function redirect(string $url, int $delay = 0): void
public function flash(string $type, string $message): void
public function dispatch(string $event, array $data = []): void
public function isWireRequest(Request $request): bool
public function isReactive(): bool
ForgeWire uses a JSON-based protocol for client-server communication with advanced features like shared state updates.
{
"id": "fw-0b4c5e3d9e83",
"controller": "App\\Controllers\\ForgeWireExamplesController",
"action": "incrementShared",
"args": [],
"dirty": {},
"depends": ["counter"],
"checksum": "632b5d500042bb4361657ea9f2991132d8a18208d0e38bf6a801f127f48f4e8c",
"fingerprint": {
"path": "/forge-wire-examples"
}
}
{
"html": "<div fw:id=\"fw-0b4c5e3d9e83\"...>...</div>",
"state": {},
"checksum": "632b5d500042bb4361657ea9f2991132d8a18208d0e38bf6a801f127f48f4e8c",
"events": [{"name": "counterUpdated", "data": {"value": 5}}],
"redirect": null,
"flash": [{"type": "success", "message": "Counter updated"}],
"updates": [
{
"id": "fw-secondary-island",
"html": "<div fw:id=\"fw-secondary-island\">Updated content</div>",
"state": {},
"checksum": "abc123..."
}
]
}
updates array contains HTML for affected islandsForgeWire includes several performance optimizations for production use.
Instead of re-rendering entire islands, use fw:target for partial updates:
<div <?= fw_id('counter') ?>>
<button fw:click="increment">+</button>
<!-- Only this div updates -->
<div fw:target>Count: <?= $count ?></div>
</div>
Show loading indicators during long-running actions using fw:loading:
<div <?= fw_id('uploader') ?>>
<button fw:click="upload">Upload File</button>
<div fw:loading class="loading-spinner">
Processing... Please wait
</div>
<div fw:target><?= $status ?></div>
</div>
Component recipes are built once per class and cached in memory:
self::$recipe[$class]Components only poll when visible on screen:
Input events have lower priority than actions:
ForgeWire follows PHP conventions and explicit permission patterns for secure, predictable server-side reactivity.
Complete examples of ForgeWire components based on real implementations.
Based on ForgeWireExamplesController - shows all major features:
#[Middleware("web")]
#[Reactive]
final class ForgeWireExamplesController
{
use ControllerHelper;
#[State]
public int $pollCount = 0;
#[State]
public int $counter = 0;
#[State(shared: true)]
public int $sharedCounter = 0;
#[State]
public string $immediateValue = '';
#[State]
public string $lazyValue = '';
#[State]
public string $debounceValue = '';
#[State]
#[Validate('required|min:3', messages: ['required' => 'Name is required', 'min' => 'Name must be at least :value characters'])]
public string $formName = '';
#[State]
#[Validate('required|email', messages: ['required' => 'Email is required', 'email' => 'Please enter a valid email address'])]
public string $formEmail = '';
#[Action]
public function onPoll(): void
{
$this->pollCount++;
}
#[Action]
public function increment(): void
{
$this->counter += $this->step;
}
#[Action(submit: true)]
public function saveForm(): void
{
$this->formMessage = 'Form saved successfully at ' . date('H:i:s');
}
#[Action]
public function handleEnter(): void
{
$this->lastKey = 'Enter pressed at ' . date('H:i:s');
}
}
Real-world example from ForgeEvents module showing advanced patterns:
#[Reactive]
#[Middleware('web')]
#[Middleware('auth')]
final class QueueController
{
use ControllerHelper;
use ReactiveControllerHelper;
#[State(shared: true)]
public array $jobs = [];
#[State]
public string $statusFilter = '';
#[State]
public string $queueFilter = '';
#[State]
public string $search = '';
#[State]
public int $currentPage = 1;
#[State]
public array $selectedJobs = [];
#[State]
public bool $showJobModal = false;
public function __construct(
private readonly QueueHubService $queueService
) {}
#[Route("/hub/queues")]
public function index(): Response
{
$this->loadJobs();
$this->loadStats();
return $this->view("pages/hub/queues", [
'jobs' => $this->jobs,
'selectedJobs' => $this->selectedJobs,
'filters' => [
'status' => $this->statusFilter,
'queue' => $this->queueFilter,
'search' => $this->search,
],
]);
}
#[Action]
public function filterJobs(string $status, string $queue): void
{
$this->statusFilter = $status;
$this->queueFilter = $queue;
$this->currentPage = 1;
$this->loadJobs();
}
#[Action]
public function selectJob(int $jobId): void
{
$this->selectedJobs[] = $jobId;
}
#[Action]
public function bulkDelete(): void
{
foreach ($this->selectedJobs as $jobId) {
$this->queueService->deleteJob($jobId);
}
$this->selectedJobs = [];
$this->loadJobs();
}
private function loadJobs(): void
{
$filters = [
'status' => $this->statusFilter,
'queue' => $this->queueFilter,
'search' => $this->search,
];
$this->paginator = $this->queueService->getJobs(
$filters,
$this->sortColumn,
$this->sortDirection,
$this->currentPage,
$this->perPage
);
$this->jobs = $this->paginator->items();
}
}
ForgeWire works with standard PHP controllers - no special component classes required:
#[Middleware("web")]
#[Reactive]
final class CounterController
{
use ControllerHelper;
#[State]
public int $count = 0;
#[Route("/counter")]
public function index(): Response
{
return $this->view("pages/counter", [
'count' => $this->count,
// Normal PHP works perfectly
'doubled' => $this->count * 2,
'status' => $this->getStatus(),
]);
}
#[Action]
public function increment(): void
{
$this->count++;
}
// Normal PHP method - no special attributes needed
private function getStatus(): string
{
return $this->count > 10 ? 'high' : 'normal';
}
}
ForgeWire includes powerful CLI tools for rapid development.
# Interactive wizard
forgewire:island
# Direct generation
forgewire:island --type=app --name=interactive-counter --kind=component
forgewire:island --type=app --name=admin/dashboard --kind=page
forgewire:island --type=module --module=ForgeEvents --name=queue-stats --kind=component
# Generate minified production version
forgewire:minify
# Custom input/output paths
forgewire:minify --input=custom/path/forgewire.js --output=production/forgewire.min.js
Optimization: Removes comments, unnecessary whitespace, and optimizes JavaScript patterns for production deployment.
The CLI generates complete reactive controller and view templates with proper attributes already in place:
// Generated Controller
#[Middleware("web")]
#[Reactive]
final class InteractiveCounterController
{
use ControllerHelper;
use ReactiveControllerHelper;
#[State]
public int $count = 0;
#[Route("/interactive-counter")]
public function index(): Response
{
// Pass data to view explicitly - just like normal PHP
return $this->view("pages/interactive-counter", [
'count' => $this->count,
// Any computed values work normally
'doubled' => $this->count * 2,
'items' => $this->getItems(),
]);
}
#[Action]
public function increment(): void
{
$this->count++;
}
// Normal PHP methods work normally
private function getItems(): array
{
return ['item1', 'item2'];
}
}
The CLI provides comprehensive setup guidance:
ForgeWire works with any PHP organization pattern - modules, traits, standard controllers:
// Example from ForgeEvents module
namespace App\Modules\ForgeEvents\Controllers\Hub;
use App\Modules\ForgeEvents\Controllers\Hub\Traits\QueueJobActions;
use App\Modules\ForgeEvents\Controllers\Hub\Traits\QueueBulkActions;
use App\Modules\ForgeEvents\Services\QueueHubService;
use App\Modules\ForgeWire\Attributes\Reactive;
use App\Modules\ForgeWire\Traits\ReactiveControllerHelper;
#[Reactive]
#[Middleware('web')]
#[Middleware('auth')]
final class QueueController
{
use ControllerHelper;
use ReactiveControllerHelper;
use QueueJobActions;
use QueueBulkActions;
#[State(shared: true)]
public array $jobs = [];
#[Route("/hub/queues")]
public function index(): Response
{
// Standard PHP controller logic
$this->loadJobs();
return $this->view("pages/hub/queues", [
'jobs' => $this->jobs,
// All other data passed explicitly
]);
}
#[Action]
public function bulkDelete(): void
{
// Standard method implementation
foreach ($this->selectedJobs as $jobId) {
$this->queueService->deleteJob($jobId);
}
$this->selectedJobs = [];
$this->loadJobs();
}
}
Here are some approaches that work well with ForgeWire. Feel free to use whatever works best for you.
Performance Win: Shared state updates are highly efficient - one server call updates all dependent islands simultaneously.