PHPackages                             dancycodes/gale - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. dancycodes/gale

ActiveLibrary[Templating &amp; Views](/categories/templating)

dancycodes/gale
===============

Laravel-native reactive frontends using Alpine Gale. Build dynamic UIs with Blade templates and Server-Sent Events.

v0.5.4(yesterday)1167↑23.3%MITJavaScriptPHP ^8.3

Since Nov 23Pushed yesterdayCompare

[ Source](https://github.com/dancycodes/gale)[ Packagist](https://packagist.org/packages/dancycodes/gale)[ Docs](https://github.com/dancycodes/gale)[ RSS](/packages/dancycodes-gale/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (9)Versions (23)Used By (0)

Laravel Gale
============

[](#laravel-gale)

[![CI](https://github.com/dancycodes/gale/actions/workflows/ci.yml/badge.svg)](https://github.com/dancycodes/gale/actions/workflows/ci.yml)[![Latest Version](https://camo.githubusercontent.com/aaf4cc16f18e08bc0f5ec3a7d49ac7b5dd782a390be8717c57188ddee98b5857/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f64616e6379636f6465732f67616c653f7374796c653d666c61742d737175617265266c6162656c3d7061636b6167697374)](https://packagist.org/packages/dancycodes/gale)[![PHP Version](https://camo.githubusercontent.com/b62a93fb4f213eea83a8e52bb4c5461696e4a6b91d7452ce2487abfd70659c7b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532422d3737374242343f7374796c653d666c61742d737175617265266c6f676f3d706870)](https://php.net)[![Laravel Version](https://camo.githubusercontent.com/09c44d755b97d7c239da2d0cca137a25d84799eb168b4ce5a5c85e0bb486fcf0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d313125324225323025374325323031322532422d4646324432303f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c)](https://laravel.com)[![Alpine.js](https://camo.githubusercontent.com/05853a99c2dd64e701352021efea3c06bb012967c6fca59ecd9ffe87a140401c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f416c70696e652e6a732d332e782d3842433044303f7374796c653d666c61742d737175617265266c6f676f3d616c70696e652e6a73)](https://alpinejs.dev)[![License](https://camo.githubusercontent.com/458425f8985b0b0c8a736cffe75e05a098e3d77906acddbcad2bfc54492a4e02/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e2e7376673f7374796c653d666c61742d737175617265)](LICENSE)

**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** = **G**ouater + **A**nais + **L**oic + **E**unice (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`](docs/README.md) | [Getting Started](docs/getting-started.md) | [Backend API](docs/backend-api.md) | [Frontend API](docs/frontend-api.md)

---

Table of Contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [How It Works](#how-it-works)
    - [Dual-Mode Architecture](#dual-mode-architecture)
    - [Request/Response Flow](#requestresponse-flow)
    - [RFC 7386 JSON Merge Patch](#rfc-7386-json-merge-patch)
- [Mode Configuration](#mode-configuration)
    - [HTTP vs SSE Comparison](#http-vs-sse-comparison)
    - [Choosing a Mode](#choosing-a-mode)
    - [Configuring the Default Mode](#configuring-the-default-mode)
    - [Per-Request Mode Override](#per-request-mode-override)
- [Backend: Laravel Gale](#backend-laravel-gale)
    - [The gale() Helper](#the-gale-helper)
    - [State Management](#state-management)
    - [DOM Manipulation](#dom-manipulation)
    - [Blade Fragments](#blade-fragments)
    - [Redirects](#redirects)
    - [Navigation](#navigation)
    - [Events and JavaScript](#events-and-javascript)
    - [Component Targeting](#component-targeting)
    - [Streaming Mode (SSE)](#streaming-mode-sse)
    - [Request Macros](#request-macros)
    - [Blade Directives](#blade-directives)
    - [Validation](#validation)
    - [Conditional Execution](#conditional-execution)
    - [Route Discovery](#route-discovery)
- [Frontend: Alpine Gale](#frontend-alpine-gale)
    - [The $action Magic](#the-action-magic)
    - [State Synchronization (x-sync)](#state-synchronization-x-sync)
    - [CSRF Protection](#csrf-protection)
    - [Global State ($gale)](#global-state-gale)
    - [Element State ($fetching)](#element-state-fetching)
    - [Loading Directives](#loading-directives)
    - [Navigation](#navigation-1)
    - [Component Registry](#component-registry)
    - [Form Binding (x-name)](#form-binding-x-name)
    - [File Uploads](#file-uploads)
    - [Message Display](#message-display)
    - [Polling (x-interval)](#polling-x-interval)
    - [Confirmation Dialogs](#confirmation-dialogs)
- [Configuration Reference](#configuration-reference)
- [Advanced Topics](#advanced-topics)
    - [DOM Patching Modes](#dom-patching-modes)
    - [View Transitions API](#view-transitions-api)
    - [SSE Protocol Specification](#sse-protocol-specification)
    - [State Serialization](#state-serialization)
- [API Reference](#api-reference)
- [Troubleshooting](#troubleshooting)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)

---

Requirements
------------

[](#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
-----------

[](#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:**

```
>

        @gale

            +

```

Click the button. The count updates via HTTP. No page reload, no JavaScript written.

---

Installation
------------

[](#installation)

```
composer require dancycodes/gale
php artisan gale:install
```

Add `@gale` to your layout's ``:

```

    @gale

```

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

[](#existing-alpinejs-projects)

Gale bundles Alpine.js (v3) with the Morph plugin. If you already have Alpine.js installed, **remove it** to prevent conflicts:

```

```

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

[](#using-additional-alpine-plugins)

Gale exposes `window.Alpine`, so other plugins work normally:

```

    @gale

        document.addEventListener('alpine:init', () => {
            Alpine.plugin(yourPlugin);
        });

```

### Optional: Publish Configuration

[](#optional-publish-configuration)

```
php artisan vendor:publish --tag=gale-config
```

---

How It Works
------------

[](#how-it-works)

### Dual-Mode Architecture

[](#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

[](#requestresponse-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

[](#rfc-7386-json-merge-patch)

State updates follow [RFC 7386](https://tools.ietf.org/html/rfc7386):

Server SendsCurrent StateResult`{ 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 `null` removes the property
- **Deep merge**: Nested objects merge recursively

---

Mode Configuration
------------------

[](#mode-configuration)

### HTTP vs SSE Comparison

[](#http-vs-sse-comparison)

FeatureHTTP Mode (Default)SSE Mode (Opt-in)**Transport**Standard JSON over HTTPServer-Sent Events stream**Response type**`application/json``text/event-stream`**Hosting**Works everywhereRequires SSE-compatible hosting**CDN / Load Balancer**Fully compatibleMay require configuration**Serverless**Fully compatibleNot recommended**Latency**Single responseStreaming (events sent as they occur)**Progress updates**Not supportedReal-time progress**Long-running ops**Subject to timeoutStream indefinitely**Connection overhead**New connection per requestHeld open during stream**Error handling**Standard HTTP status codesInline error events**Retry**Automatic with backoffBuilt-in SSE reconnection**Best for**Forms, CRUD, navigation, most interactionsDashboards, progress bars, chat, AI streaming### Choosing a Mode

[](#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 (&lt; 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

[](#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

[](#per-request-mode-override)

On the frontend, override per request:

```

Process

Save
```

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

[](#backend-laravel-gale)

### The gale() Helper

[](#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-management)

#### state()

[](#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()

[](#patchstate)

Alias for `state()` when passing an array -- preferred for explicit multi-key patches:

```
gale()->patchState(['count' => 1, 'updated' => true]);
```

#### forget()

[](#forget)

Remove state properties (sends `null` per RFC 7386):

```
gale()->forget('tempData');
gale()->forget(['tempData', 'cache', 'draft']);
```

#### messages()

[](#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()

[](#clearmessages)

Clear all messages:

```
gale()->clearMessages();
```

#### flash()

[](#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:

```

```

### DOM Manipulation

[](#dom-manipulation)

#### view()

[](#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()

[](#html)

Patch raw HTML into the DOM:

```
gale()->html('New content');

gale()->html('New item', [
    'selector' => '#list',
    'mode' => 'append',
]);
```

#### DOM Convenience Methods

[](#dom-convenience-methods)

```
// Server-driven state (replacement via initTree)
gale()->outer('#element', 'Replaced');
gale()->inner('#container', 'Inner content');

// Client-preserved state (smart morphing via Alpine.morph)
gale()->outerMorph('#element', 'Updated');
gale()->innerMorph('#container', 'Morphed content');

// Insertion modes
gale()->append('#list', 'Last');
gale()->prepend('#list', 'First');
gale()->before('#target', 'Before');
gale()->after('#target', 'After');

// Removal
gale()->remove('.deprecated');

// Viewport modifiers (optional third parameter)
gale()->append('#chat', $html, ['scroll' => 'bottom']);
gale()->outer('#form', $html, ['show' => 'top']);
```

MethodModeState 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:**

OptionTypeDefaultDescription`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

[](#blade-fragments)

Extract and render specific sections from Blade views without rendering the entire template.

**Define fragments in Blade:**

```

    @fragment('todo-items')
    @foreach($todos as $todo)
        {{ $todo->title }}
    @endforeach
    @endfragment

```

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

[](#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();
```

MethodDescription`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

[](#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

[](#events-and-javascript)

#### dispatch()

[](#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:

```

```

#### js()

[](#js)

Execute JavaScript in the browser:

```
gale()->js('console.log("Hello from server")');
gale()->js('myApp.showNotification("Saved!")');
```

#### debug()

[](#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

[](#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)

[](#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

[](#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

[](#blade-directives)

#### @gale

[](#gale)

Include the JavaScript bundle and CSRF meta tag:

```

    @gale

```

Accepts optional options:

```
@gale(['nonce' => config('gale.csp_nonce')])
```

#### @fragment / @endfragment

[](#fragment--endfragment)

Define extractable fragments:

```
@fragment('header')
{{ $title }}
@endfragment
```

#### @ifgale / @else / @endifgale

[](#ifgale--else--endifgale)

Conditional rendering based on request type:

```
@ifgale
    {{ $content }}
@else
    @include('layouts.app')
@endifgale
```

### Validation

[](#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

[](#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

[](#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
---------------------

[](#frontend-alpine-gale)

All frontend features require an Alpine.js context (`x-data` or `x-init`).

### The $action Magic

[](#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.

```

    +1

    GET
    POST
    PUT
    PATCH
    DELETE

```

CSRF tokens are automatically injected for all non-GET methods. No manual token handling required.

#### Request Options

[](#request-options)

```
Save
```

OptionTypeDefaultDescription`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)

[](#state-synchronization-x-sync)

The `x-sync` directive controls which Alpine state properties are sent to the server:

```

```

x-sync ValueResult`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 directiveSend nothing (use `include` option if needed)### CSRF Protection

[](#csrf-protection)

The `@gale` directive adds ``. 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)

[](#global-state-gale)

The `$gale` magic provides global connection state:

```

    Loading...
    Reconnecting...

        Error:

    Clear Errors

```

PropertyTypeDescription`loading`boolAny request in progress`activeCount`numberNumber of active requests`retrying`boolCurrently retrying a request`retriesFailed`boolAll retries exhausted`error`boolHas any error`lastError`stringMost recent error message`errors`arrayAll error messages`clearErrors()`functionClear all errors### Element State ($fetching)

[](#element-state-fetching)

Track per-element loading state:

```

    Save
    Saving...

```

Note: `$fetching` is a function -- always use `$fetching()` with parentheses.

### Loading Directives

[](#loading-directives)

#### x-loading

[](#x-loading)

Show/hide elements or apply classes during loading:

```
Loading...
Content visible when not loading
Submit
Submit
Loading (delayed)...
```

#### x-indicator

[](#x-indicator)

Bind a boolean state variable to loading activity:

```

        Save
        Saving...

```

### Navigation

[](#navigation-1)

#### x-navigate Directive

[](#x-navigate-directive)

Enable SPA navigation on links and forms:

```
Users
Sort by Name
Users (replace history)

Users

    Search

    Submit

```

ModifierDescription`.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

[](#navigate-magic)

```
Users

Navigate
```

#### x-navigate-skip

[](#x-navigate-skip)

Exclude specific links from navigation:

```

    Dashboard
    External Link

```

### Component Registry

[](#component-registry)

Named components that can be targeted from the backend or other components.

```

    Cart loaded

    Clear
    Recalculate

```

MethodDescription`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)

[](#form-binding-x-name)

Combines `x-model` behavior with automatic state creation and `name` attributes:

```

    Login

```

Supports nested paths, checkboxes, radios, selects, and modifiers:

```

```

### File Uploads

[](#file-uploads)

```

        Name:
        Size:

    Upload

```

MagicDescription`$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

[](#message-display)

Display validation errors and notifications from the server:

```

    Subscribe

```

Array validation with dynamic paths:

```

```

### Polling (x-interval)

[](#polling-x-interval)

Run expressions at configurable intervals:

```

...

    Processing...

```

### Confirmation Dialogs

[](#confirmation-dialogs)

```

    Delete

```

---

Configuration Reference
-----------------------

[](#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  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' | ''
    '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:**

VariableDefaultDescription`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 `` tags in patched HTML`GALE_MORPH_MARKERS``true`Inject Blade morph anchor comments`GALE_CSP_NONCE``null`CSP nonce value---

Advanced Topics
---------------

[](#advanced-topics)

**DOM Patching Modes**Gale provides 9 DOM patching modes in three categories:

CategoryModesState 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:

EventPurpose`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 ...

```

**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 `$`.

```

    Save User

```

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

[](#api-reference)

**GaleResponse Methods**MethodDescription`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**MacroDescription`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**MagicDescription`$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**DirectiveDescription`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
---------------

[](#troubleshooting)

IssueCauseSolution"Multiple instances of Alpine"Duplicate Alpine.js loadedRemove existing Alpine, use `@gale` only`$action` is undefinedMagic used outside `x-data`Wrap in `x-data` elementCSRF 419 errorToken expired or missingVerify `@gale` is in ``State not updatingKey mismatchCheck `x-data` property names match server keysNavigation not workingMissing directiveAdd `x-navigate` to links or containerMessages not showingWrong keyEnsure `x-message` key matches server message keyCounter not updatingMissing `x-sync`Add `x-sync` to `x-data` element to send stateJSON shown instead of pageMissing `web: true`Add `web: true` to `gale()->view()` for page routesFor in-depth troubleshooting, see [Debug &amp; Troubleshooting](docs/debug-troubleshooting.md).

---

Testing
-------

[](#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
------------

[](#contributing)

Contributions are welcome. To contribute:

1. Fork the repository and create a feature branch
2. Write tests for any new functionality
3. Run the full test suite: `vendor/bin/pest && vendor/bin/phpstan analyse`
4. Format code: `vendor/bin/pint`
5. Submit a pull request with a clear description of the change

Report bugs via [GitHub Issues](https://github.com/dancycodes/gale/issues).

---

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE).

---

Credits
-------

[](#credits)

Created by **DancyCodes** --

- [Laravel](https://laravel.com)
- [Alpine.js](https://alpinejs.dev)
- [Datastar](https://data-star.dev) -- SSE inspiration

###  Health Score

45

—

FairBetter than 93% of packages

Maintenance100

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Total

22

Last Release

1d ago

PHP version history (2 changes)v0.1.0PHP ^8.2

v0.5.3PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/e71891915c1ba98f0604c864dbb48df12fb722eb3c21140973ccca58291d8b20?d=identicon)[dancycodes](/maintainers/dancycodes)

---

Top Contributors

[![dancycodes](https://avatars.githubusercontent.com/u/139002105?v=4)](https://github.com/dancycodes "dancycodes (192 commits)")

---

Tags

laravelreal-timebladessealpinealpinejsreactiveserver sent eventsSPAfragmentshtmx-alternativelivewire-alternativegalemedia

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/dancycodes-gale/health.svg)

```
[![Health](https://phpackages.com/badges/dancycodes-gale/health.svg)](https://phpackages.com/packages/dancycodes-gale)
```

###  Alternatives

[blade-ui-kit/blade-icons

A package to easily make use of icons in your Laravel Blade views.

2.4k31.8M214](/packages/blade-ui-kit-blade-icons)[robsontenorio/mary

Gorgeous UI components for Livewire powered by daisyUI and Tailwind

1.4k421.8k13](/packages/robsontenorio-mary)[tightenco/jigsaw

Simple static sites with Laravel's Blade.

2.2k434.0k26](/packages/tightenco-jigsaw)[victorybiz/laravel-simple-select

Laravel Simple Select inputs component for Blade and Livewire.

13720.8k](/packages/victorybiz-laravel-simple-select)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
