ForgeWire

Lightweight Livewire-like reactive components for Forge Kernel. Build dynamic interfaces with server-side interactivity, security by default, and support for complex data types.

Overview

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 Power, Standard Controller Simplicity

No separate component classes needed
Reactive standard Controllers (#[Reactive])
Security by default (explicit attributes)
Session-based state with signed checksums
Native support for DTOs and Models
Ultra-lightweight JS client

Architecture & Design Philosophy

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.

The Anatomy of a Reactive Controller

When a controller is marked with #[Reactive], ForgeWire manages its lifecycle:

  • State Hydration: Upon an action, ForgeWire restores the controller's properties from the session state.
  • Action Execution: The requested method is called with arguments provided by the frontend.
  • View Re-rendering: After the action, the controller's primary view is re-rendered with the updated state and sent back to the client.
  • Checksum Security: Every state transfer is signed. Tampering with state or calling unauthorized methods is impossible.

Basic Usage

Creating a reactive feature requires three steps: mark the controller, mark the state, and wrap the view with island identifiers.

1. The Controller

#[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++;
    }
}

2. The View with Islands

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>

3. Island Architecture Benefits

  • Required Boundaries: fw_id() creates the reactive boundary - no reactivity without it
  • Multiple Islands: Create independent reactive areas on same page
  • Targeted Updates: Use fw:target for efficient partial updates
  • Shared State: Islands can share state using #[State(shared: true)] with fw:depends
  • Independent Polling: Each island can have its own polling interval

Attributes System

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.

#[Reactive]

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.

#[State(shared: bool)]

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.

#[Action(submit: bool)]

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.

#[Validate]

Applied to properties. Validation rules - defines validation rules and custom error messages for properties that receive user input.

State Management

#[State(shared: bool)]

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.

Shared State Across Islands

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)].

Shared State Implementation

// 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>

How Shared State Works

  • Mark Shared: Add #[State(shared: true) to properties that should be shared
  • Opt-In Dependencies: Islands use fw:depends="prop1,prop2" to specify which shared properties they need
  • Automatic Updates: When shared state changes, only islands that depend on that property are re-rendered
  • Multiple Dependencies: Islands can depend on multiple shared properties separated by commas

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.

Frontend Directives

ForgeWire uses HTML attributes (directives) to bind your UI to your controller.

Binding Data: fw:model

  • 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

Handling Events

  • 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

Passing Arguments: fw:param-*

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>

Security: The Sandbox

ForgeWire operates on a "deny-by-default" principle:

  • Checksum signing: All state sent to the browser is HMAC-signed with your APP_KEY. If a user modifies the state in the console, the server rejects the request.
  • Method Whitelisting: Only #[Action] methods can be invoked. Even protected/private methods are strictly blocked.
  • Fingerprinting: ForgeWire tracks the controller class and current URL path to prevent "component injection" or cross-page state reuse.

Actions & Methods

The #[Action] Attribute

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
}

Calling Actions from Frontend

<!-- 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>

Validation

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 = '';

Validation Features

  • Automatic Validation: Runs when properties marked with #[Validate] are updated
  • Custom Messages: Provide user-friendly error messages for each rule
  • Parameter Replacement: Use :value placeholders in messages
  • Form Submission: Use #[Action(submit: true)] for form handling

Validation runs automatically when dirty state includes the property.

Frontend Integration

ForgeWire's JavaScript client handles all frontend interactions automatically.

Directives Reference

Event Handling

  • 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

Data Binding

  • 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

Polling & Updates

  • fw:poll — Auto-refresh every 2 seconds
  • fw:poll.5s — Custom poll interval
  • fw:poll.3s fw:action="onPoll" — Custom action on poll

Island Management

  • 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

Shared State

  • 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.

Arguments & Parameters

  • fw:param-id="123" — Pass parameters to actions
  • fw:param-name="value" — String parameters
  • fw:param-count="5" — Numeric parameters

Island Management

  • 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

Advanced Directives

<!-- 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>

Example HTML

<div fw:id="fw-0b4c5e3d9e83" >
    <div class="counter">
        <button fw:click="decrement">–</button>
        <span>2 (even)</span>
        <button fw:click="increment">+</button>
    </div>
</div>

Polling with IntersectionObserver

Components only poll when visible on screen (performance optimization):

<div fw:id="..." fw:poll.5s>
    <!-- Only polls when visible -->
</div>

ForgeWire Architecture & Reactive Protocol

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.

Core Principles

Permission-Based Attributes

Attributes work as permissions, not data exposure

  • • #[Reactive] = Permission to monitor
  • • #[Action] = Permission to call
  • • #[State] = Permission to persist
  • • No automatic data exposure

Island Boundaries Required

Reactivity only works with explicit island boundaries

  • • fw_id() creates reactive boundary
  • • No reactivity without fw_id()
  • • Multiple islands per page
  • • Explicit data passing required

CLI Tools & Generation

# 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

ReactiveControllerHelper Trait

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

Wire Protocol

ForgeWire uses a JSON-based protocol for client-server communication with advanced features like shared state updates.

Request Format

{
  "id": "fw-0b4c5e3d9e83",
  "controller": "App\\Controllers\\ForgeWireExamplesController",
  "action": "incrementShared",
  "args": [],
  "dirty": {},
  "depends": ["counter"],
  "checksum": "632b5d500042bb4361657ea9f2991132d8a18208d0e38bf6a801f127f48f4e8c",
  "fingerprint": {
    "path": "/forge-wire-examples"
  }
}

Response Format

{
  "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..."
    }
  ]
}

Advanced Protocol Features

  • Shared State Updates: Multiple islands can update simultaneously when shared state changes
  • Partial Updates: updates array contains HTML for affected islands
  • Event Dispatching: Client-side events can be triggered from server
  • Flash Messages: Session-based flash messages for user feedback
  • Redirect Support: Server-initiated redirects with optional delays

Performance Optimizations

ForgeWire includes several performance optimizations for production use.

Targeted Updates with fw:target

Instead of re-rendering entire islands, use fw:target for partial updates:

  • Only the marked element gets updated on server response
  • Dramatically reduces HTML payload size
  • Perfect for counters, status messages, progress indicators
<div <?= fw_id('counter') ?>>
    <button fw:click="increment">+</button>
    <!-- Only this div updates -->
    <div fw:target>Count: <?= $count ?></div>
</div>

Loading States

Show loading indicators during long-running actions using fw:loading:

  • Automatically shows during action execution
  • Hidden when action completes
  • Provides immediate user feedback
<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>

Recipe Caching

Component recipes are built once per class and cached in memory:

  • No repeated reflection on every request
  • Recipe defines hydration/dehydration strategy
  • Cached in static array: self::$recipe[$class]

IntersectionObserver Polling

Components only poll when visible on screen:

  • Uses browser IntersectionObserver API
  • Stops polling when component is off-screen
  • Resumes automatically when visible again
  • Saves server resources

Request Prioritization

Input events have lower priority than actions:

  • Permission-Based Attributes: Attributes work as permissions, not data exposure
  • • #[Reactive] = Permission to monitor
  • • #[Action] = Permission to call
  • • #[State] = Permission to persist
  • • #[Validate] = Validation rules
  • • No automatic data exposure

Design Philosophy & Principles

ForgeWire follows PHP conventions and explicit permission patterns for secure, predictable server-side reactivity.

Security by Default

  • Explicit Permissions: Attributes act as permission gates
  • No Auto-Exposure: Nothing is exposed without explicit attributes
  • Checksum Protection: All state transfers are signed and verified
  • Action Whitelisting: Only #[Action] methods can be called

PHP Conventions

  • Standard Controllers: No special component classes needed
  • Explicit Data Flow: You control what data reaches views
  • Native PHP Types: Works with PHP 8.3+ type system
  • Framework Agnostic: Built for Forge, follows PHP standards

Performance Focused

  • Island Boundaries: Only re-render what changes
  • Targeted Updates: fw:target for partial updates
  • Recipe Caching: No repeated reflection overhead
  • Intersection Observer: Smart polling when visible

Developer Experience

  • CLI Generation: forgewire:island command for scaffolding
  • Clear Errors: Validation with custom messages
  • Reactive Helper: Built-in methods for common patterns
  • Debug Support: Comprehensive debugging and error handling

Usage Examples

Complete examples of ForgeWire components based on real implementations.

Comprehensive Example Controller

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');
    }
}

Complex Reactive Controller - Queue Management

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();
    }
}

Standard PHP Controller

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';
    }
}

CLI Commands & Tools

ForgeWire includes powerful CLI tools for rapid development.

Generate Islands

# 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

Production Minification

# 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.

Generated Structure

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'];
    }
}

Setup Guide

The CLI provides comprehensive setup guidance:

  • Reactive Setup: How to add #[Reactive] and why it's required
  • Action Exposure: How to use #[Action] to expose methods to browser
  • State Management: How to use #[State] for property persistence
  • Normal PHP: Everything else works like regular PHP

Modular Organization

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();
    }
}

Suggestions

Here are some approaches that work well with ForgeWire. Feel free to use whatever works best for you.

Things That Often Help

  • • Use #[State] for property persistence
  • • Keep islands focused and single-purpose
  • • Write normal PHP for derived values (no special attributes needed)
  • • Leverage fw:model.debounce for search inputs
  • • Use fw:target for performance optimization
  • • Always use fw:depends with shared state
  • • Use ReactiveControllerHelper trait for helper methods
  • • Validate all user input with #[Validate]
  • • Use CLI generators for consistent structure

Things to Consider

  • • Remember fw:depends for shared state
  • • Be mindful of sensitive data exposure
  • • Consider session size with large state objects
  • • Don't forget #[Action] on callable methods
  • • Polling frequency affects performance
  • • Security matters with shared state
  • • Validation helps catch issues early
  • • CLI generators save time and provide structure

Performance Considerations

  • Targeted Updates: fw:target can reduce HTML payload
  • Input Strategies: fw:model.lazy and fw:model.debounce for different use cases
  • Polling Impact: Intersection observer helps with visibility-based polling
  • Shared State Efficiency: All dependent islands update in single request - no multiple HTTP requests needed
  • No Event Dispatching: Shared state changes don't need manual event dispatching - ForgeWire handles coordination automatically
  • Session Size: Large state objects affect session storage

Performance Win: Shared state updates are highly efficient - one server call updates all dependent islands simultaneously.