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 it
fw:target
for efficient partial updates
#[State(shared: true)]
with
fw:depends
ForgeWire 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 kernel 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 shared
fw:depends="prop1,prop2"
to specify which shared properties they need
Use 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 inactivity
fw:model.defer="name"
- Updates only when an action is triggered
fw:click="save"
- Call action on click
fw: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 handling
Validation 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 key
fw:keydown.escape="cancel"
— Call action on Escape key
fw:model="property"
— Two-way binding (immediate)
fw:model.lazy
— Update on blur/change
fw:model.debounce
— Debounced updates (600ms default)
fw:model.debounce.300ms
— Custom debounce time
fw:poll
— Auto-refresh every 2 seconds
fw:poll.5s
— Custom poll interval
fw:poll.3s fw:action="onPoll"
— Custom action on poll
fw:id("name")
— PHP helper to create unique island
identifier
fw:target
— Marks element for partial updates
(performance optimization)
fw:loading
— Shows content during action processing
fw: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 actions
fw:param-name="value"
— String parameters
fw:param-count="5"
— Numeric parameters
fw:id("name")
— PHP helper to create unique island
identifier
fw: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 islands
ForgeWire 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.