monkeyscloud/monkeyslegion-live

Server-driven reactive components — write PHP 8.4, get a reactive UI with MonkeysJS

Maintainers

Package info

github.com/MonkeysCloud/MonkeysLegion-Live

pkg:composer/monkeyscloud/monkeyslegion-live

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-05-27 00:52 UTC

This package is auto-updated.

Last update: 2026-05-27 00:53:27 UTC


README

Server-driven reactive components for MonkeysLegion. Write PHP, get a reactive UI — no API, no SPA build step.

monkeyslegion-live is the server half. MonkeysJS is the client runtime that ships with it. Together they give you Livewire/LiveComponent-class interactivity: components are plain PHP 8.4 classes, the UI updates over the wire, and you never hand-write fetch calls or JSON endpoints.

The differentiator: MonkeysJS is a purpose-built runtime, not a wrapper around Alpine.js or Stimulus. It's designed against this exact wire protocol — hydration, morphing, and batching are first-class. No PHP framework ships streamed AI rendering from a server into the DOM token-by-token.

Installation

composer require monkeyscloud/monkeyslegion-live

Add to your layout:

@liveScripts          {{-- injects MonkeysJS + CSRF + config --}}
@liveStyles           {{-- optional: loading/transition styles --}}

Requirements

  • PHP 8.4+
  • monkeyscloud/monkeyslegion-template ^2.0
  • monkeyscloud/monkeyslegion-http ^2.1
  • monkeyscloud/monkeyslegion-router ^2.1
  • monkeyscloud/monkeyslegion-di ^2.0
  • monkeyscloud/monkeyslegion-encryption ^1.0

Optional

  • monkeyslegion-validation — live form validation via WithValidation
  • monkeyslegion-permissions#[RequiresPermission] on actions
  • monkeyslegion-sockets — real-time push via Broadcasts
  • monkeyslegion-apex — streamed AI rendering via Streams
  • monkeyslegion-files — chunked file uploads via WithFileUploads

Quick Start

1. Write a component

<?php
declare(strict_types=1);

namespace App\Live;

use MonkeysLegion\Live\LiveComponent;
use MonkeysLegion\Live\Attributes\State;
use MonkeysLegion\Live\Attributes\Action;
use MonkeysLegion\Live\Attributes\Computed;

final class Counter extends LiveComponent
{
    #[State] public int $count = 0;
    #[State] public int $step = 1;

    #[Action]
    public function increment(): void
    {
        $this->count += $this->step;
    }

    #[Action]
    public function decrement(): void
    {
        $this->count -= $this->step;
    }

    #[Computed]
    public function parity(): string
    {
        return $this->count % 2 === 0 ? 'even' : 'odd';
    }

    public function render(): string
    {
        return $this->view('live.counter');
    }
}

2. Write the template

{{-- resources/views/live/counter.mlv --}}
<div>
    <p>Count: <strong>{{ $count }}</strong> ({{ $this->parity() }})</p>
    <button ml:click="decrement"></button>
    <button ml:click="increment">+</button>
    <label>
        Step: <input type="number" ml:model.live="step" min="1">
    </label>
</div>

3. Use it

@live(App\Live\Counter::class)
@live(App\Live\Counter::class, { count: 10, step: 5 })

Directive Set

Directive Purpose
ml:model Two-way bind input to #[State]
ml:model.live Sync on every input event
ml:model.live.debounce.300ms Debounced live sync
ml:model.blur Sync on blur
ml:model.lazy Sync on change
ml:click Call #[Action] method
ml:submit.prevent Form submission action
ml:keydown.enter Key event → action
ml:loading Show during round-trip
ml:loading.remove Hide during round-trip
ml:loading.attr.disabled Set attribute during loading
ml:loading.delay.200ms Delayed loading indicator
ml:dirty Reflect unsynced changes
ml:poll.5s Re-render on interval
ml:poll.keep-alive.30s Poll even when tab hidden
ml:offline Show when offline
ml:online Show when online
ml:transition CSS enter/leave transitions
ml:ignore Opt subtree out of morphing
ml:replace Force full replacement
ml:preserve Keep node identical across morphs
ml:stream Target for streamed content

Modifiers: .prevent, .stop, .self, .debounce.<ms>, .throttle.<ms>, .once, .window, .outside

Component API

State

#[State] public string $name = '';
#[State(persist: true)] public string $theme = 'light';   // survives navigation
#[State(url: true)] public string $tab = 'overview';      // synced to query string
#[State(readonly: true)] public int $userId;               // signed, never accepted back
#[State(defer: true)] public array $data = [];             // lazy-fetched

Actions

#[Action]
public function save(): void { /* ... */ }

#[Action(confirm: 'Delete permanently?')]
public function delete(int $id): void { /* ... */ }

#[Action(renderless: true)]
public function trackClick(): void { /* ... */ }   // skip re-render

Computed

#[Computed]
public function fullName(): string
{
    return $this->firstName . ' ' . $this->lastName;
}

#[Computed(cache: true)]
public function expensiveReport(): array { /* ... */ }

Lifecycle Hooks

public function mount(int $postId): void {}      // once, on initial load
public function hydrate(): void {}               // every request, after snapshot restored
public function dehydrate(): void {}             // every request, before snapshot sent
public function updating(string $prop, $value): void {}
public function updated(string $prop, $value): void {}
public function rendering(): void {}
public function rendered(string $html): void {}

Property-specific hooks: updatedEmail(), updatingStatus().

Events

$this->emit('saved', $id);                // to all components
$this->emitUp('child:done');              // to parent only
$this->emitSelf('refresh');               // to self only
$this->emitTo(Sidebar::class, 'sync');    // to specific component
$this->dispatchBrowser('confetti', ['count' => 50]);  // browser CustomEvent

Validation

use MonkeysLegion\Live\Concerns\WithValidation;

final class RegisterForm extends LiveComponent
{
    use WithValidation;

    #[State] public string $email = '';
    #[State] public string $password = '';

    protected function rules(): array
    {
        return [
            'email'    => 'required|email|unique:users,email',
            'password' => 'required|min:12',
        ];
    }

    #[Action]
    public function register(): void
    {
        $data = $this->validate();
        // create user…
    }

    public function updatedEmail(): void
    {
        $this->validateOnly('email');
    }
}
<input ml:model.live.debounce.400ms="email">
@error('email') <span class="err">{{ $message }}</span> @enderror

Streaming AI (Apex Integration)

use MonkeysLegion\Live\Concerns\Streams;

final class AiChat extends LiveComponent
{
    use Streams;

    #[State] public array $messages = [];
    #[State] public string $prompt = '';

    #[Action]
    public function send(Apex $apex): void
    {
        $this->messages[] = ['role' => 'user', 'content' => $this->prompt];
        $this->prompt = '';

        $this->stream('reply', function (StreamTarget $out) use ($apex) {
            foreach ($apex->pipeline('chat')->stream($this->messages) as $token) {
                $out->append($token);
            }
        });
    }
}
<div ml:stream="reply" class="assistant-msg"></div>

Testing

use MonkeysLegion\Live\Testing\LiveTest;

final class CounterTest extends TestCase
{
    use LiveTest;

    public function test_increments(): void
    {
        Live::test(Counter::class)
            ->assertSee('0')
            ->set('step', 5)
            ->call('increment')
            ->assertSet('count', 5);
    }

    public function test_validation(): void
    {
        Live::test(RegisterForm::class)
            ->set('email', 'not-an-email')
            ->call('register')
            ->assertHasErrors(['email'])
            ->assertNoRedirect();
    }
}

CLI

ml make:live Counter                  # component + template stub
ml make:live Posts/Editor --form      # with WithValidation scaffold
ml live:list                          # registered components
ml live:assets                        # (re)publish MonkeysJS runtime

Configuration

Copy config/live.example.mlc to your project's config/live.mlc. See the file for all options.

Security Model

Concern Mitigation
State tampering HMAC checksum on every snapshot; mismatch → 419
Calling private methods Only #[Action] methods are routable
Mass-assignment Only #[State] (non-readonly) properties accept client updates
Argument injection Action args type-coerced against method signature
Authorization #[RequiresPermission] runs before the action body
Entity tampering Entities rehydrate by id with checksum
CSRF Token injected by @liveScripts, validated on every POST
File-upload abuse Size/MIME allowlists enforced server-side

License

MIT © MonkeysCloud