dancycodes/gale
Composer 安装命令:
composer require dancycodes/gale
包简介
Laravel-native reactive frontends using Alpine Gale. Build dynamic UIs with Blade templates and Server-Sent Events.
关键字:
README 文档
README
Laravel Gale is a server-driven reactive framework for Laravel. It uses standard HTTP responses (JSON) by default to deliver real-time UI updates to Alpine.js components directly from your Blade templates -- no JavaScript framework, no build complexity, no API layer. For long-running operations or real-time streaming, Server-Sent Events (SSE) is available as an explicit opt-in.
GALE = Gouater + Anais + Loic + Eunice (Founders' initials)
This README documents both:
- Laravel Gale -- The PHP backend package (
dancycodes/gale) - Alpine Gale -- The Alpine.js frontend plugin (bundled with Laravel Gale)
Full documentation: docs/README.md | Getting Started | Backend API | Frontend API
Table of Contents
- Requirements
- Quick Start
- Installation
- How It Works
- Mode Configuration
- Backend: Laravel Gale
- Frontend: Alpine Gale
- Configuration Reference
- Advanced Topics
- API Reference
- Troubleshooting
- Testing
- Contributing
- License
Requirements
- PHP 8.2 or higher
- Laravel 11 or 12
- Alpine.js 3.x (bundled -- no separate install needed)
No Node.js or npm required for basic usage. @gale serves the pre-built JS bundle from public/vendor/gale/.
Quick Start
A complete reactive counter in under 20 lines:
routes/web.php:
Route::get('/counter', fn() => gale()->view('counter', ['count' => 0], web: true)); Route::post('/increment', function () { return gale()->state('count', request()->state('count', 0) + 1); });
resources/views/counter.blade.php:
<!DOCTYPE html> <html> <head> @gale </head> <body> <div x-data="{ count: {{ $count }} }" x-sync> <span x-text="count"></span> <button @click="$action('/increment')">+</button> </div> </body> </html>
Click the button. The count updates via HTTP. No page reload, no JavaScript written.
Installation
composer require dancycodes/gale php artisan gale:install
Add @gale to your layout's <head>:
<head> @gale </head>
That's it. The @gale directive outputs:
- CSRF meta tag
- Alpine.js (v3) with the Morph plugin
- The Alpine Gale plugin
- Debug panel (when
APP_DEBUG=true)
Existing Alpine.js Projects
Gale bundles Alpine.js (v3) with the Morph plugin. If you already have Alpine.js installed, remove it to prevent conflicts:
<!-- Remove any CDN script --> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
// Remove these lines from resources/js/app.js: import Alpine from 'alpinejs'; window.Alpine = Alpine; Alpine.start();
Then use @gale instead -- it handles everything.
Using Additional Alpine Plugins
Gale exposes window.Alpine, so other plugins work normally:
<head> @gale <script> document.addEventListener('alpine:init', () => { Alpine.plugin(yourPlugin); }); </script> </head>
Optional: Publish Configuration
php artisan vendor:publish --tag=gale-config
How It Works
Dual-Mode Architecture
Gale operates in two modes with an identical developer API:
-
HTTP mode (default): Responses are standard JSON payloads (
Content-Type: application/json). Simple, works with all hosting environments, CDNs, and load balancers. Suitable for the vast majority of interactions. -
SSE mode (opt-in): Responses are streamed as Server-Sent Events (
Content-Type: text/event-stream). Required for long-running operations, real-time progress, or live streaming. Activated per-request with{ sse: true }or globally via configuration.
The backend API is identical in both modes -- the same gale()->state(), gale()->view(), and all other methods work regardless of transport. The frontend automatically detects the response type and processes accordingly.
Request/Response Flow
BROWSER
+----------------------------------------------------+
| Alpine.js Component (x-data) |
| State: { count: 0, user: {...} } |
+----------------------------------------------------+
|
| @click="$action('/increment')"
v
+----------------------------------------------------+
| HTTP Request |
| Headers: Gale-Request: true, X-CSRF-TOKEN |
| Body: { count: 0, user: {...} } |
+----------------------------------------------------+
|
v
LARAVEL SERVER
+----------------------------------------------------+
| Controller |
| $count = request()->state('count'); |
| return gale()->state('count', $count + 1); |
+----------------------------------------------------+
|
+------------+------------+
| |
HTTP Mode SSE Mode
+------------------+ +--------------------+
| application/json | | text/event-stream |
| { events: [...] }| | event: gale-patch |
+------------------+ +--------------------+
| |
+------------+------------+
|
v
+----------------------------------------------------+
| Alpine.js merges state via RFC 7386 |
| State: { count: 1, user: {...} } |
| UI reactively updates |
+----------------------------------------------------+
RFC 7386 JSON Merge Patch
State updates follow RFC 7386:
| Server Sends | Current State | Result |
|---|---|---|
{ count: 5 } |
{ count: 0, name: "John" } |
{ count: 5, name: "John" } |
{ name: null } |
{ count: 0, name: "John" } |
{ count: 0 } |
{ user: { email: "new" } } |
{ user: { name: "John", email: "old" } } |
{ user: { name: "John", email: "new" } } |
- Values merge: Sent values replace existing values
- Null deletes: Sending
nullremoves the property - Deep merge: Nested objects merge recursively
Mode Configuration
HTTP vs SSE Comparison
| Feature | HTTP Mode (Default) | SSE Mode (Opt-in) |
|---|---|---|
| Transport | Standard JSON over HTTP | Server-Sent Events stream |
| Response type | application/json |
text/event-stream |
| Hosting | Works everywhere | Requires SSE-compatible hosting |
| CDN / Load Balancer | Fully compatible | May require configuration |
| Serverless | Fully compatible | Not recommended |
| Latency | Single response | Streaming (events sent as they occur) |
| Progress updates | Not supported | Real-time progress |
| Long-running ops | Subject to timeout | Stream indefinitely |
| Connection overhead | New connection per request | Held open during stream |
| Error handling | Standard HTTP status codes | Inline error events |
| Retry | Automatic with backoff | Built-in SSE reconnection |
| Best for | Forms, CRUD, navigation, most interactions | Dashboards, progress bars, chat, AI streaming |
Choosing a Mode
Use HTTP mode (default) when:
- Building forms, CRUD operations, or standard interactions
- Deploying to serverless, CDN-fronted, or shared hosting
- You want the simplest possible setup
- Response times are fast (< 1 second)
Use SSE mode when:
- You need real-time progress updates (file processing, imports)
- Building live dashboards or chat interfaces
- Streaming AI-generated content
- Operations take more than a few seconds
Configuring the Default Mode
The default mode can be set at three levels (highest priority first):
1. Request header (per-request, set automatically by frontend):
Gale-Mode: sse
2. Environment variable (application-wide):
GALE_MODE=http
3. Config file (config/gale.php):
return [ 'mode' => env('GALE_MODE', 'http'), // ... ];
Per-Request Mode Override
On the frontend, override per request:
<!-- Force SSE for this action --> <button @click="$action('/process', { sse: true })">Process</button> <!-- Force HTTP for this action --> <button @click="$action('/save', { http: true })">Save</button>
Or use gale()->stream() on the backend, which always uses SSE regardless of configuration:
return gale()->stream(function ($gale) { // This always streams via SSE $gale->state('progress', 50); });
Backend: Laravel Gale
The gale() Helper
Returns a request-scoped GaleResponse instance with a fluent API:
return gale() ->state('count', 42) ->state('updated', now()->toISOString()) ->messages(['_success' => 'Saved!']);
The same instance accumulates events throughout the request. In HTTP mode, they are serialized as a single JSON response. In SSE mode, they are streamed as individual events.
State Management
state()
Set state values to merge into the Alpine component:
// Single key-value gale()->state('count', 42); // Multiple values gale()->state([ 'count' => 42, 'user' => ['name' => 'John', 'email' => 'john@example.com'], ]); // Nested update (merges with existing) gale()->state('user.email', 'new@example.com'); // Only set if key doesn't exist in component state gale()->state('defaults', ['theme' => 'dark'], ['onlyIfMissing' => true]);
patchState()
Alias for state() when passing an array -- preferred for explicit multi-key patches:
gale()->patchState(['count' => 1, 'updated' => true]);
forget()
Remove state properties (sends null per RFC 7386):
gale()->forget('tempData'); gale()->forget(['tempData', 'cache', 'draft']);
messages()
Set the messages state object (used for validation errors and notifications):
gale()->messages([ 'email' => 'Invalid email address', 'password' => 'Password too short', ]); // Success pattern gale()->messages(['_success' => 'Profile saved!']);
clearMessages()
Clear all messages:
gale()->clearMessages();
flash()
Deliver flash data to both the session and the _flash Alpine state key in one call:
gale()->flash('status', 'Your account has been updated.'); gale()->flash(['status' => 'ok', 'message' => 'Saved!']);
In the view, display flash reactively:
<div x-data="{ _flash: {} }" x-sync="['_flash']"> <div x-show="_flash.status" x-text="_flash.status" class="alert"></div> </div>
DOM Manipulation
view()
Render a Blade view and patch it into the DOM:
// Morph by matching element IDs gale()->view('partials.user-card', ['user' => $user]); // With selector and mode gale()->view('partials.item', ['item' => $item], [ 'selector' => '#items-list', 'mode' => 'append', ]); // As web fallback for non-Gale requests gale()->view('dashboard', $data, web: true);
html()
Patch raw HTML into the DOM:
gale()->html('<div id="content">New content</div>'); gale()->html('<li>New item</li>', [ 'selector' => '#list', 'mode' => 'append', ]);
DOM Convenience Methods
// Server-driven state (replacement via initTree) gale()->outer('#element', '<div id="element">Replaced</div>'); gale()->inner('#container', '<p>Inner content</p>'); // Client-preserved state (smart morphing via Alpine.morph) gale()->outerMorph('#element', '<div id="element">Updated</div>'); gale()->innerMorph('#container', '<p>Morphed content</p>'); // Insertion modes gale()->append('#list', '<li>Last</li>'); gale()->prepend('#list', '<li>First</li>'); gale()->before('#target', '<div>Before</div>'); gale()->after('#target', '<div>After</div>'); // Removal gale()->remove('.deprecated'); // Viewport modifiers (optional third parameter) gale()->append('#chat', $html, ['scroll' => 'bottom']); gale()->outer('#form', $html, ['show' => 'top']);
| Method | Mode | State Handling |
|---|---|---|
outer($selector, $html, $opts) |
outer |
Server-driven |
inner($selector, $html, $opts) |
inner |
Server-driven |
outerMorph($selector, $html, $opts) |
outerMorph |
Client-preserved |
innerMorph($selector, $html, $opts) |
innerMorph |
Client-preserved |
append($selector, $html, $opts) |
append |
New elements init |
prepend($selector, $html, $opts) |
prepend |
New elements init |
before($selector, $html, $opts) |
before |
New elements init |
after($selector, $html, $opts) |
after |
New elements init |
remove($selector) |
remove |
Cleanup |
View options:
| Option | Type | Default | Description |
|---|---|---|---|
selector |
string | null |
CSS selector for target element |
mode |
string | 'outer' |
DOM patching mode |
useViewTransition |
bool | false |
Enable View Transitions API |
settle |
int | 0 |
Delay (ms) before patching |
scroll |
string | null |
Auto-scroll: 'top' or 'bottom' |
show |
string | null |
Scroll into viewport: 'top' or 'bottom' |
focusScroll |
bool | false |
Maintain focus scroll position |
Blade Fragments
Extract and render specific sections from Blade views without rendering the entire template.
Define fragments in Blade:
<div id="todo-list"> @fragment('todo-items') @foreach($todos as $todo) <div id="todo-{{ $todo->id }}">{{ $todo->title }}</div> @endforeach @endfragment </div>
Render fragments:
// Single fragment gale()->fragment('todos', 'todo-items', ['todos' => $todos]); // With options gale()->fragment('todos', 'todo-items', ['todos' => $todos], [ 'selector' => '#todo-list', 'mode' => 'morph', ]); // Multiple fragments at once gale()->fragments([ ['view' => 'dashboard', 'fragment' => 'stats', 'data' => $statsData], ['view' => 'dashboard', 'fragment' => 'recent-orders', 'data' => $ordersData], ]);
Redirects
Full-page browser redirects with session flash support:
return gale()->redirect('/dashboard'); return gale()->redirect('/dashboard') ->with('message', 'Welcome back!') ->with(['key' => 'value']); return gale()->redirect('/register') ->withErrors($validator) ->withInput();
| Method | Description |
|---|---|
with($key, $value) |
Flash data to session |
withInput($input) |
Flash form input for repopulation |
withErrors($errors) |
Flash validation errors |
back($fallback) |
Redirect to previous URL with fallback |
backOr($route, $params) |
Back with named route fallback |
refresh($query, $fragment) |
Reload current page |
home() |
Redirect to root URL |
route($name, $params) |
Redirect to named route |
intended($default) |
Redirect to auth intended URL |
forceReload($bypass) |
Hard reload via JavaScript |
Navigation
Trigger SPA navigation from the backend:
gale()->navigate('/users'); gale()->navigate('/users', 'main-content'); // Merge query params gale()->navigateMerge(['page' => 2]); // Replace history instead of push gale()->navigateReplace('/users'); // Update query parameters in place gale()->updateQueries(['sort' => 'name', 'order' => 'asc']); // Clear specific query parameters gale()->clearQueries(['filter', 'search']); // Full page reload gale()->reload();
Events and JavaScript
dispatch()
Dispatch custom DOM events from the server:
gale()->dispatch('user-updated', ['id' => $user->id]); // Targeted to a specific element gale()->dispatch('refresh', ['section' => 'cart'], [ 'selector' => '.shopping-cart', ]);
Listen in Alpine:
<div x-data @user-updated.window="handleUpdate($event.detail)"></div>
js()
Execute JavaScript in the browser:
gale()->js('console.log("Hello from server")'); gale()->js('myApp.showNotification("Saved!")');
debug()
Send debug data to the Gale debug panel (dev mode only):
gale()->debug('payload', $request->all()); gale()->debug(['user' => $user, 'state' => $state]);
Component Targeting
Target specific named Alpine components from the backend:
// Update a component's state gale()->componentState('cart', [ 'items' => $cartItems, 'total' => $total, ]); // Invoke a method on a named component gale()->componentMethod('cart', 'recalculate'); gale()->componentMethod('calculator', 'setValues', [10, 20, 30]);
Streaming Mode (SSE)
For long-running operations, stream events in real-time. gale()->stream() always uses SSE regardless of the global mode setting:
return gale()->stream(function ($gale) { $users = User::cursor(); $total = User::count(); $processed = 0; foreach ($users as $user) { $user->processExpensiveOperation(); $processed++; // Sent immediately to the browser $gale->state('progress', [ 'current' => $processed, 'total' => $total, 'percent' => round(($processed / $total) * 100), ]); } $gale->state('complete', true); $gale->messages(['_success' => "Processed {$total} users"]); });
Request Macros
Gale registers these macros on the Laravel Request object:
// Check if the request is a Gale request if (request()->isGale()) { return gale()->state('data', $data); } return view('page', compact('data')); // Access state sent from the Alpine component $count = request()->state('count', 0); $email = request()->state('user.email'); // Check if it's a navigation request if (request()->isGaleNavigate()) { return gale()->fragment('page', 'content', $data); } // Validate state with automatic error response $validated = request()->validateState([ 'email' => 'required|email', 'name' => 'required|min:2', ]);
Blade Directives
@gale
Include the JavaScript bundle and CSRF meta tag:
<head> @gale </head>
Accepts optional options:
@gale(['nonce' => config('gale.csp_nonce')])
@fragment / @endfragment
Define extractable fragments:
@fragment('header') <header>{{ $title }}</header> @endfragment
@ifgale / @else / @endifgale
Conditional rendering based on request type:
@ifgale <div id="content">{{ $content }}</div> @else @include('layouts.app') @endifgale
Validation
Standard Laravel validation works reactively for Gale requests. ValidationException is automatically converted to a gale()->messages() response:
// Standard validate() -- auto-converts for Gale requests public function store(Request $request) { $request->validate([ 'state.name' => 'required|min:2|max:255', 'state.email' => 'required|email|unique:users', ]); // Process... } // validateState() -- validates against component state directly public function store(Request $request) { $validated = $request->validateState([ 'name' => 'required|min:2|max:255', 'email' => 'required|email|unique:users', ]); User::create($validated); return gale()->messages(['_success' => 'Account created!']); }
Form Request classes also work out of the box:
// app/Http/Requests/StoreUserRequest.php class StoreUserRequest extends FormRequest { public function rules(): array { return [ 'state.name' => 'required|min:2', 'state.email' => 'required|email', ]; } } // Controller -- validation errors auto-converted for Gale public function store(StoreUserRequest $request) { User::create($request->validated()); return gale()->messages(['_success' => 'Created!']); }
Conditional Execution
gale()->when($condition, function ($gale) { $gale->state('visible', true); }); gale()->whenGale( fn($g) => $g->state('partial', true), fn($g) => $g->web(view('full')) ); gale()->whenGaleNavigate('sidebar', function ($gale) { $gale->fragment('layout', 'sidebar', $data); }); // Web fallback for non-Gale requests return gale() ->state('data', $data) ->web(view('page', compact('data')));
Route Discovery
Optional attribute-based route discovery:
// config/gale.php 'route_discovery' => [ 'enabled' => true, 'discover_controllers_in_directory' => [ app_path('Http/Controllers'), ], ],
use Dancycodes\Gale\Routing\Attributes\Route; use Dancycodes\Gale\Routing\Attributes\Prefix; use Dancycodes\Gale\Routing\Attributes\Group; use Dancycodes\Gale\Routing\Attributes\Middleware; #[Prefix('/admin')] class UserController extends Controller { #[Route('GET', '/users', name: 'admin.users.index')] public function index() { } #[Route('GET', '/users/{id}', name: 'admin.users.show')] public function show($id) { } } // Group attribute (prefix + middleware + route name prefix in one) #[Group(prefix: '/api', middleware: ['auth'], as: 'api.')] class ApiController extends Controller { #[Route('GET', '/data')] public function data() { } }
List discovered routes:
php artisan gale:routes php artisan gale:routes --json
Frontend: Alpine Gale
All frontend features require an Alpine.js context (x-data or x-init).
The $action Magic
The $action magic handles all HTTP requests. It defaults to POST with automatic CSRF injection -- the most common pattern for server actions.
<div x-data="{ count: 0 }" x-sync> <!-- Default: POST with CSRF --> <button @click="$action('/increment')">+1</button> <!-- Method shorthands --> <button @click="$action.get('/api/data')">GET</button> <button @click="$action.post('/api/save')">POST</button> <button @click="$action.put('/api/replace')">PUT</button> <button @click="$action.patch('/api/update')">PATCH</button> <button @click="$action.delete('/api/remove')">DELETE</button> </div>
CSRF tokens are automatically injected for all non-GET methods. No manual token handling required.
Request Options
<button @click="$action('/save', { include: ['user', 'settings'], exclude: ['tempData'], headers: { 'X-Custom': 'value' }, sse: true, retryInterval: 1000, retryMaxCount: 10, requestCancellation: true, debounce: 300, throttle: 500, onProgress: (percent) => console.log(percent) })">Save</button>
| Option | Type | Default | Description |
|---|---|---|---|
method |
string | 'POST' |
HTTP method |
include |
string[] | -- | Only send these state keys |
exclude |
string[] | -- | Don't send these state keys |
headers |
object | {} |
Additional request headers |
sse |
bool | false |
Force SSE mode for this request |
http |
bool | false |
Force HTTP mode for this request |
retryInterval |
number | 1000 |
Initial retry delay (ms) |
retryScaler |
number | 2 |
Exponential backoff multiplier |
retryMaxWaitMs |
number | 30000 |
Maximum retry delay (ms) |
retryMaxCount |
number | 10 |
Maximum retry attempts |
requestCancellation |
bool | false |
Cancel previous in-flight request |
debounce |
number | -- | Trailing-edge debounce (ms) |
throttle |
number | -- | Leading-edge throttle (ms) |
onProgress |
function | -- | Upload progress callback (0-100) |
State Synchronization (x-sync)
The x-sync directive controls which Alpine state properties are sent to the server:
<!-- Send everything --> <div x-data="{ name: '', email: '', open: false }" x-sync> <!-- Send specific keys only --> <div x-data="{ name: '', email: '', open: false }" x-sync="['name', 'email']"> <!-- String syntax shorthand --> <div x-data="{ name: '', email: '' }" x-sync="name, email"> <!-- No x-sync = send nothing automatically --> <div x-data="{ name: '', temp: null }">
| x-sync Value | Result |
|---|---|
x-sync (empty) |
Send all state (wildcard) |
x-sync="*" |
Send all state (explicit wildcard) |
x-sync="['a','b']" |
Send only a and b |
x-sync="a, b" |
Send only a and b (string syntax) |
| No directive | Send nothing (use include option if needed) |
CSRF Protection
The @gale directive adds <meta name="csrf-token">. The $action magic reads this token automatically for all non-GET requests.
// Custom CSRF configuration (rarely needed) Alpine.gale.configureCsrf({ headerName: 'X-CSRF-TOKEN', metaName: 'csrf-token', cookieName: 'XSRF-TOKEN', });
Global State ($gale)
The $gale magic provides global connection state:
<div x-data> <div x-show="$gale.loading">Loading...</div> <div x-show="$gale.retrying">Reconnecting...</div> <div x-show="$gale.error"> Error: <span x-text="$gale.lastError"></span> </div> <span x-text="$gale.activeCount + ' requests active'"></span> <button @click="$gale.clearErrors()">Clear Errors</button> </div>
| Property | Type | Description |
|---|---|---|
loading |
bool | Any request in progress |
activeCount |
number | Number of active requests |
retrying |
bool | Currently retrying a request |
retriesFailed |
bool | All retries exhausted |
error |
bool | Has any error |
lastError |
string | Most recent error message |
errors |
array | All error messages |
clearErrors() |
function | Clear all errors |
Element State ($fetching)
Track per-element loading state:
<button @click="$action('/save')" :disabled="$fetching()"> <span x-show="!$fetching()">Save</span> <span x-show="$fetching()">Saving...</span> </button>
Note: $fetching is a function -- always use $fetching() with parentheses.
Loading Directives
x-loading
Show/hide elements or apply classes during loading:
<div x-loading>Loading...</div> <div x-loading.remove>Content visible when not loading</div> <button x-loading.class="opacity-50">Submit</button> <button x-loading.attr="disabled">Submit</button> <div x-loading.delay.200ms>Loading (delayed)...</div>
x-indicator
Bind a boolean state variable to loading activity:
<div x-data="{ saving: false }" x-indicator="saving"> <button @click="$action('/save')" :disabled="saving"> <span x-show="!saving">Save</span> <span x-show="saving">Saving...</span> </button> </div>
Navigation
x-navigate Directive
Enable SPA navigation on links and forms:
<a href="/users" x-navigate>Users</a> <a href="/users?sort=name" x-navigate.merge>Sort by Name</a> <a href="/users" x-navigate.replace>Users (replace history)</a> <!-- Navigation key for partial updates --> <a href="/users" x-navigate.key.sidebar>Users</a> <!-- Forms --> <form action="/search" method="GET" x-navigate> <input name="q" type="text" /> <button type="submit">Search</button> </form> <!-- POST form navigation (PRG pattern) --> <form action="/submit" method="POST" x-navigate> <input name="name" type="text" /> <button type="submit">Submit</button> </form>
| Modifier | Description |
|---|---|
.merge |
Merge query params with current URL |
.replace |
Replace history entry instead of push |
.key.{name} |
Navigation key for targeted updates |
.only.{params} |
Keep only these query params |
.except.{params} |
Remove these query params |
.debounce.{ms} |
Debounce navigation |
.throttle.{ms} |
Throttle navigation |
$navigate Magic
<button @click="$navigate('/users')">Users</button> <button @click="$navigate('/users', { merge: true, replace: true, key: 'main-content' })">Navigate</button>
x-navigate-skip
Exclude specific links from navigation:
<nav x-navigate> <a href="/dashboard">Dashboard</a> <a href="/external" x-navigate-skip>External Link</a> </nav>
Component Registry
Named components that can be targeted from the backend or other components.
<div x-data="{ items: [], total: 0 }" x-component="cart"> <span x-text="total"></span> </div> <!-- Access from another component --> <div x-data> <span x-show="$components.has('cart')">Cart loaded</span> <span x-text="$components.state('cart', 'total')"></span> <button @click="$components.update('cart', { total: 0 })">Clear</button> <button @click="$invoke('cart', 'recalculate')">Recalculate</button> </div>
| Method | Description |
|---|---|
get(name) |
Get component Alpine data object |
has(name) |
Check if component exists |
all() |
Get all registered components |
getByTag(tag) |
Get components with tag |
state(name, property) |
Get reactive state value |
update(name, state) |
Merge state into component |
create(name, state) |
Set state (with onlyIfMissing option) |
delete(name, keys) |
Remove state keys |
invoke(name, method, ...args) |
Call method on component |
watch(name, property, callback) |
Watch for changes |
when(name, timeout?) |
Promise resolving when component exists |
onReady(name, callback) |
Callback when component ready |
Form Binding (x-name)
Combines x-model behavior with automatic state creation and name attributes:
<div x-data="{ email: '', password: '' }"> <input x-name="email" type="email"> <input x-name="password" type="password"> <button @click="$action('/login')">Login</button> </div>
Supports nested paths, checkboxes, radios, selects, and modifiers:
<input x-name="user.name" type="text"> <input x-name.lazy="search" type="text"> <input x-name.number="quantity" type="text"> <input x-name.trim="username" type="text"> <input x-name.array="tags" type="checkbox" value="alpha">
File Uploads
<div x-data> <input type="file" name="avatar" x-files /> <div x-show="$file('avatar')"> <p>Name: <span x-text="$file('avatar')?.name"></span></p> <p>Size: <span x-text="$formatBytes($file('avatar')?.size)"></span></p> <img :src="$filePreview('avatar')" /> </div> <button @click="$action('/upload')">Upload</button> </div>
| Magic | Description |
|---|---|
$file(name) |
Get single file info |
$files(name) |
Get array of files |
$filePreview(name, index?) |
Get preview URL |
$clearFiles(name?) |
Clear file input(s) |
$formatBytes(size, decimals?) |
Format bytes to human-readable |
$uploading |
Upload in progress |
$uploadProgress |
Progress 0-100 |
Message Display
Display validation errors and notifications from the server:
<div x-data="{ messages: {} }"> <input x-name="email" type="email"> <span x-message="email" class="text-red-500"></span> <div x-message="_success" class="text-green-500"></div> <button @click="$action('/subscribe')">Subscribe</button> </div>
Array validation with dynamic paths:
<template x-for="(item, index) in items" :key="index"> <div> <input x-model="items[index].name"> <span x-message="`items.${index}.name`" class="text-red-500"></span> </div> </template>
Polling (x-interval)
Run expressions at configurable intervals:
<!-- Increment every second --> <div x-data="{ count: 0 }" x-interval.1s="count++"> <span x-text="count"></span> </div> <!-- Poll server every 5 seconds --> <div x-data="{ status: '' }" x-interval.5s="$action.get('/api/status')"> <span x-text="status"></span> </div> <!-- Only run when tab is visible --> <div x-interval.visible.5s="$action.get('/api/status')">...</div> <!-- Stop on condition --> <div x-data="{ done: false, progress: 0 }" x-interval.1s="progress += 10; done = progress >= 100" x-interval-stop="done"> Processing... </div>
Confirmation Dialogs
<button @click="$action.delete('/item/1')" x-confirm="Are you sure?"> Delete </button>
Configuration Reference
After running php artisan vendor:publish --tag=gale-config, edit config/gale.php:
return [ // Default response mode: 'http' (JSON) or 'sse' (Server-Sent Events) 'mode' => env('GALE_MODE', 'http'), // Intercept dd() and dump() during Gale requests, render in debug panel 'debug' => env('GALE_DEBUG', false), // Sanitize HTML in gale-patch-elements events (XSS protection) 'sanitize_html' => env('GALE_SANITIZE_HTML', true), // Allow <script> tags in patched HTML (false = strip scripts) 'allow_scripts' => env('GALE_ALLOW_SCRIPTS', false), // Inject HTML comment markers for conditional/loop Blade blocks // Improves morph accuracy; disable in production to reduce payload 'morph_markers' => env('GALE_MORPH_MARKERS', true), // Content Security Policy nonce: null | 'auto' | '<nonce-string>' 'csp_nonce' => env('GALE_CSP_NONCE', null), // Security headers added to all Gale responses 'headers' => [ 'x_content_type_options' => 'nosniff', 'x_frame_options' => 'SAMEORIGIN', 'cache_control' => 'no-store, no-cache, must-revalidate', 'custom' => [], ], // Open-redirect prevention 'redirect' => [ 'allowed_domains' => [], // e.g. ['payment.stripe.com', '*.myapp.com'] 'allow_external' => false, 'log_blocked' => true, ], // Attribute-based route discovery (opt-in) 'route_discovery' => [ 'enabled' => false, 'conventions' => true, // Auto-discover index/show/create/store/edit/update/destroy 'discover_controllers_in_directory' => [ // app_path('Http/Controllers'), ], 'discover_views_in_directory' => [], 'pending_route_transformers' => [ ...Dancycodes\Gale\Routing\Config::defaultRouteTransformers(), ], ], ];
Environment variables:
| Variable | Default | Description |
|---|---|---|
GALE_MODE |
http |
Default response mode (http or sse) |
GALE_DEBUG |
false |
Enable debug panel and dd()/dump() interception |
GALE_SANITIZE_HTML |
true |
Sanitize patched HTML for XSS |
GALE_ALLOW_SCRIPTS |
false |
Allow <script> tags in patched HTML |
GALE_MORPH_MARKERS |
true |
Inject Blade morph anchor comments |
GALE_CSP_NONCE |
null |
CSP nonce value |
Advanced Topics
DOM Patching Modes
Gale provides 9 DOM patching modes in three categories:
| Category | Modes | State Handling |
|---|---|---|
| Server-driven | outer (default), inner |
State from server HTML via initTree() |
| Client-preserved | outerMorph, innerMorph |
Existing Alpine state preserved via Alpine.morph() |
| Insertion/Deletion | before, after, prepend, append, remove |
New elements initialized |
Use outer when the server controls all state (forms, server-rendered content).
Use outerMorph when client state must survive the update (counters, toggles, focus).
Backward compatibility: replace() maps to outer(), morph() maps to outerMorph().
HTMX-compatible aliases: outerHTML = outer, innerHTML = inner, beforebegin = before, afterend = after, afterbegin = prepend, beforeend = append, delete = remove.
View Transitions API
Enable smooth page transitions via the browser's View Transitions API:
gale()->view('page', $data, ['useViewTransition' => true]);
Global configuration:
Alpine.gale.configure({ viewTransitions: true }); // enabled by default
Falls back gracefully in unsupported browsers.
SSE Protocol Specification
When using SSE mode, Gale streams these event types:
| Event | Purpose |
|---|---|
gale-patch-state |
Merge state into Alpine component |
gale-patch-elements |
DOM manipulation |
gale-patch-component |
Update named component |
gale-invoke-method |
Call method on component |
gale-patch-state format:
event: gale-patch-state
data: state {"count":1}
data: onlyIfMissing false
gale-patch-elements format:
event: gale-patch-elements
data: selector #content
data: mode outer
data: elements <div id="content">...</div>
gale-patch-component format:
event: gale-patch-component
data: component cart
data: state {"total":42}
gale-invoke-method format:
event: gale-invoke-method
data: component cart
data: method recalculate
data: args [10,20]
State Serialization
When making requests, Alpine Gale serializes the component's x-data based on x-sync:
Serialized: Properties in x-sync, form fields with name attribute, nested objects, arrays.
Not serialized: Functions, DOM elements, circular references, properties starting with _ or $.
<div x-data="{ user: {...}, temp: null }" x-sync="['user']"> <button @click="$action('/save')">Save User</button> <!-- Only { user: {...} } is sent --> </div>
Global Configuration API
Alpine.gale.configure({ defaultMode: 'http', // 'http' | 'sse' viewTransitions: true, // Enable View Transitions API foucTimeout: 3000, // Max ms to wait for stylesheets during navigation navigationIndicator: true, // Show progress bar during navigation pauseOnHidden: true, // Pause SSE when tab is hidden pauseOnHiddenDelay: 1000, // Debounce delay before pausing (ms) settleDuration: 0, // Swap-settle transition delay (ms) csrfRefresh: 'auto', // CSRF refresh strategy: 'auto' | 'meta' | 'sanctum' retry: { maxRetries: 3, // Max retry attempts for network errors initialDelay: 1000, // Initial retry delay (ms) backoffMultiplier: 2, // Exponential backoff multiplier }, redirect: { allowedDomains: [], // Trusted external redirect domains allowExternal: false, // Allow all external redirects logBlocked: true, // Log blocked redirects }, });
Morph Lifecycle Hooks
Register callbacks to run before/after DOM morphing. Useful for preserving third-party library state (Chart.js, GSAP, TipTip, Sortable):
const cleanup = Alpine.gale.onMorph({ beforeUpdate(el, toEl) { // Called before element is updated // Return false to prevent the update }, afterUpdate(el) { // Called after element is updated myChart.update(); }, beforeRemove(el) { // Called before element is removed // Return false to prevent removal }, afterRemove(el) { // Cleanup after removal }, }); // Remove hooks when component is destroyed cleanup();
API Reference
GaleResponse Methods
| Method | Description |
|---|---|
state($key, $value, $options) |
Set state to merge into component |
patchState($state) |
Set multiple state keys (alias for state(array)) |
forget($keys) |
Remove state keys |
messages($messages) |
Set messages state |
clearMessages() |
Clear messages |
flash($key, $value) |
Flash to session + Alpine _flash state |
debug($label, $data) |
Send debug data to debug panel |
view($view, $data, $options, $web) |
Render and patch Blade view |
fragment($view, $fragment, $data, $options) |
Render named fragment |
fragments($fragments) |
Render multiple fragments |
html($html, $options, $web) |
Patch raw HTML |
outer($selector, $html, $options) |
Replace element (server state) |
inner($selector, $html, $options) |
Replace inner content (server state) |
outerMorph($selector, $html, $options) |
Morph element (preserve state) |
innerMorph($selector, $html, $options) |
Morph children (preserve state) |
append($selector, $html, $options) |
Append HTML |
prepend($selector, $html, $options) |
Prepend HTML |
before($selector, $html, $options) |
Insert before element |
after($selector, $html, $options) |
Insert after element |
remove($selector) |
Remove element |
js($script, $options) |
Execute JavaScript |
dispatch($event, $data, $options) |
Dispatch DOM event |
navigate($url, $key, $options) |
Trigger SPA navigation |
navigateMerge($params, $key) |
Navigate merging query params |
navigateReplace($url, $key) |
Navigate replacing history |
updateQueries($params, $key) |
Update query params in place |
clearQueries($keys) |
Clear query params |
reload() |
Full page reload |
componentState($name, $state, $options) |
Update component state |
componentMethod($name, $method, $args) |
Call component method |
redirect($url) |
Create redirect response |
stream($callback) |
Stream mode (always SSE) |
when($condition, $true, $false) |
Conditional execution |
unless($condition, $callback) |
Inverse conditional |
whenGale($gale, $web) |
Gale request conditional |
whenNotGale($callback) |
Non-Gale conditional |
whenGaleNavigate($key, $callback) |
Navigate conditional |
web($response) |
Set web fallback response |
reset() |
Clear all accumulated events |
Request Macros
| Macro | Description |
|---|---|
isGale() |
Check if request is a Gale request |
state($key, $default) |
Get state from component |
isGaleNavigate($key) |
Check if navigation request |
galeNavigateKey() |
Get navigation key |
galeNavigateKeys() |
Get all navigation keys |
validateState($rules, $messages, $attrs) |
Validate component state |
Frontend Magics
| Magic | Description |
|---|---|
$action(url, options?) |
POST with auto CSRF (default) |
$action.get(url, options?) |
GET request |
$action.post(url, options?) |
POST with auto CSRF |
$action.put(url, options?) |
PUT with auto CSRF |
$action.patch(url, options?) |
PATCH with auto CSRF |
$action.delete(url, options?) |
DELETE with auto CSRF |
$gale |
Global connection state |
$fetching() |
Element loading state (call as function) |
$navigate(url, options?) |
Programmatic navigation |
$components |
Component registry API |
$invoke(name, method, ...args) |
Invoke component method |
$file(name) |
Get file info |
$files(name) |
Get files array |
$filePreview(name, index?) |
Get preview URL |
$clearFiles(name?) |
Clear files |
$formatBytes(size, decimals?) |
Format bytes |
$uploading |
Upload in progress |
$uploadProgress |
Upload progress 0-100 |
Frontend Directives
| Directive | Description |
|---|---|
x-sync |
Sync state to server (wildcard or specific keys) |
x-navigate |
Enable SPA navigation |
x-navigate-skip |
Skip navigation handling |
x-component="name" |
Register named component |
x-name="field" |
Form binding with state |
x-files |
File input binding |
x-message="key" |
Display messages |
x-loading |
Loading state display |
x-indicator="var" |
Loading state variable |
x-interval |
Auto-polling / repeating expression |
x-interval-stop="expr" |
Stop polling condition |
x-confirm |
Confirmation dialog |
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| "Multiple instances of Alpine" | Duplicate Alpine.js loaded | Remove existing Alpine, use @gale only |
$action is undefined |
Magic used outside x-data |
Wrap in x-data element |
| CSRF 419 error | Token expired or missing | Verify @gale is in <head> |
| State not updating | Key mismatch | Check x-data property names match server keys |
| Navigation not working | Missing directive | Add x-navigate to links or container |
| Messages not showing | Wrong key | Ensure x-message key matches server message key |
| Counter not updating | Missing x-sync |
Add x-sync to x-data element to send state |
| JSON shown instead of page | Missing web: true |
Add web: true to gale()->view() for page routes |
For in-depth troubleshooting, see Debug & Troubleshooting.
Testing
# Package PHP tests cd packages/dancycodes/gale vendor/bin/phpunit # Run only unit tests vendor/bin/pest --testsuite Unit # Run only feature tests vendor/bin/pest --testsuite Feature # Static analysis vendor/bin/phpstan analyse # Code formatting vendor/bin/pint # JavaScript tests (from project root) npm test
Contributing
Contributions are welcome. To contribute:
- Fork the repository and create a feature branch
- Write tests for any new functionality
- Run the full test suite:
vendor/bin/pest && vendor/bin/phpstan analyse - Format code:
vendor/bin/pint - Submit a pull request with a clear description of the change
Report bugs via GitHub Issues.
License
MIT License. See LICENSE.
Credits
Created by DancyCodes -- dancycodes@gmail.com
dancycodes/gale 适用场景与选型建议
dancycodes/gale 是一款 基于 JavaScript 开发的 Composer 扩展包,目前已累计 218 次下载、GitHub Stars 达 1, 最近一次更新时间为 2025 年 11 月 23 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「server-sent-events」 「real-time」 「laravel」 「blade」 「reactive」 「sse」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 dancycodes/gale 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 dancycodes/gale 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 dancycodes/gale 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
A PHP 5.3 library for creating an EventSource stream.
Talk is a Laravel based realtime messaging, chatting and conversation system. It helps to develop users messaging, chatting and conversations in easy way.
Boxalino Real Time User Experience (RTUX) Magento2 plugin
Laravel package for generating Agora.io tokens (RTC, RTM, Chat) with support for both V1 and V2 token formats
PHP library for Broadcastt HTTP API
NO LIBRARIES socket per page bridge for your Laravel application.
统计信息
- 总下载量: 218
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 1
- 点击次数: 30
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-11-23