monkeyscloud / monkeyslegion-live
Server-driven reactive components — write PHP 8.4, get a reactive UI with MonkeysJS
Package info
github.com/MonkeysCloud/MonkeysLegion-Live
pkg:composer/monkeyscloud/monkeyslegion-live
Requires
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
Suggests
- monkeyscloud/monkeyslegion-apex: ^1.1 — For streamed AI token rendering via Streams concern
- monkeyscloud/monkeyslegion-files: ^2.1 — For chunked file uploads via WithFileUploads
- monkeyscloud/monkeyslegion-permissions: ^1.0 — For #[RequiresPermission] on actions
- monkeyscloud/monkeyslegion-sockets: ^1.1 — For real-time push via Broadcasts concern
- monkeyscloud/monkeyslegion-validation: ^2.0 — For live form validation via WithValidation
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.0monkeyscloud/monkeyslegion-http^2.1monkeyscloud/monkeyslegion-router^2.1monkeyscloud/monkeyslegion-di^2.0monkeyscloud/monkeyslegion-encryption^1.0
Optional
monkeyslegion-validation— live form validation viaWithValidationmonkeyslegion-permissions—#[RequiresPermission]on actionsmonkeyslegion-sockets— real-time push viaBroadcastsmonkeyslegion-apex— streamed AI rendering viaStreamsmonkeyslegion-files— chunked file uploads viaWithFileUploads
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