PHPackages                             laravelldone/sql-to-signal - 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. [Database &amp; ORM](/categories/database)
4. /
5. laravelldone/sql-to-signal

ActiveLibrary[Database &amp; ORM](/categories/database)

laravelldone/sql-to-signal
==========================

Call -&gt;toSignal() on Eloquent/Query Builder chains to get a reactive Signal object for Livewire 3/4 and Alpine.js

v1.1.1(3mo ago)22MITPHPPHP ^8.2

Since Mar 18Pushed 2mo agoCompare

[ Source](https://github.com/neon2027/sql-to-signal)[ Packagist](https://packagist.org/packages/laravelldone/sql-to-signal)[ RSS](/packages/laravelldone-sql-to-signal/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (10)Versions (6)Used By (0)

sql-to-signal
=============

[](#sql-to-signal)

Call `->toSignal()` on any Eloquent or Query Builder chain and get back a reactive `Signal` object — ready to wire into Livewire 3/4 components and Alpine.js without any boilerplate.

```
$signal = User::where('active', true)->toSignal();
```

---

Why not just `clone $query`?
----------------------------

[](#why-not-just-clone-query)

The clone pattern is the typical workaround when you need to reuse a query builder — but it falls apart quickly in Livewire and Alpine.js contexts.

### Reusing a query in a Livewire component

[](#reusing-a-query-in-a-livewire-component)

**Without `toSignal()` — clone pattern**

```
class OrderDashboard extends Component
{
    // You can't store a QueryBuilder as a public property.
    // Livewire can't serialize it — it will throw or silently drop it.
    // So you have to rebuild the query from scratch on every request.

    public array $orders = [];    // you lose Collection methods
    public int   $count  = 0;
    public ?array $first = null;

    private function baseQuery(): Builder
    {
        // Duplicated every time: if filters change you must update in multiple places
        return DB::table('orders')
            ->where('status', $this->status)
            ->where('user_id', auth()->id())
            ->orderBy('created_at', 'desc');
    }

    public function mount(): void
    {
        $q = $this->baseQuery();
        $this->orders = $q->get()->toArray();       // hit 1
        $this->count  = (clone $q)->count();        // hit 2  ← extra query
        $this->first  = (clone $q)->first();        // hit 3  ← extra query
    }

    public function refresh(): void
    {
        // Rebuild everything again — same 3 queries
        $q = $this->baseQuery();
        $this->orders = $q->get()->toArray();
        $this->count  = (clone $q)->count();
        $this->first  = (clone $q)->first();
    }
}
```

Problems:

- `clone` only works within the same request — you can't put a `Builder` in a Livewire property
- The query definition is repeated or called through a private helper — easy to drift out of sync
- 3 separate database hits to get the same data
- `toArray()` discards the model — you get raw stdClass, no Eloquent methods on rows

---

**With `toSignal()`**

```
class OrderDashboard extends Component
{
    public Signal $orders;  // serializes/hydrates automatically between requests

    public function mount(): void
    {
        // One query, one database hit. count/first/pluck come for free.
        $this->orders = DB::table('orders')
            ->where('status', $this->status)
            ->where('user_id', auth()->id())
            ->orderBy('created_at', 'desc')
            ->toSignal();
    }

    public function refresh(): void
    {
        // Re-runs the exact same SQL — no need to rebuild the query
        $this->orders = $this->orders->refresh();
    }
}
```

```
Total: {{ $orders->count() }}        {{-- no extra query --}}
First: {{ $orders->first()->id }}    {{-- no extra query --}}
```

---

### Passing data to Alpine.js

[](#passing-data-to-alpinejs)

**Without `toSignal()`**

```
// Controller / Livewire component
$rows     = DB::table('orders')->where(...)->get()->toArray();
$count    = DB::table('orders')->where(...)->count();   // cloned query, second hit
$interval = config('dashboard.polling_interval');       // manually forwarded

return view('dashboard', compact('rows', 'count', 'interval'));
```

```

```

Gotchas:

- Two database hits for the same filter
- You manually `json_encode` each piece
- Polling interval is a magic number hard to change in one place
- No single object you can pass to a sub-component or an API response

---

**With `toSignal()`**

```
$signal = DB::table('orders')->where(...)->toSignal();

return view('dashboard', compact('signal'));
```

```

    {{-- signal.data, signal.meta.count, signal.meta.polling_interval --}}
    {{-- all in one place, one query, zero manual wiring --}}

```

---

### Summary

[](#summary)

`clone $query``->toSignal()`Survives Livewire serializationNo — Builder can't be a public propertyYes — Signal hydrates/dehydrates cleanlyDatabase hits for count + firstExtra query eachZero — derived from the same CollectionRefresh in LivewireRebuild query from scratch`$signal->refresh()`Alpine.js wiringManual `json_encode` per variable`@js($signal)` — data + meta in one shotModel hydration on refreshLost — raw `stdClass`Preserved — Eloquent models rebuiltPolling interval in syncHard-coded in JSCarried in `signal.meta.polling_interval`---

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

[](#requirements)

- PHP 8.2+
- Laravel 11, 12, or 13
- Livewire 3 or 4

---

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

[](#installation)

```
composer require laravelldone/sql-to-signal
```

The service provider is auto-discovered. To publish the config file:

```
php artisan vendor:publish --tag="sql-to-signal-config"
```

---

Configuration
-------------

[](#configuration)

`config/sql-to-signal.php`:

```
return [
    'cache' => [
        'enabled' => false,
        'ttl'     => 60, // seconds
    ],

    // Passed as meta for Alpine.js polling wiring
    'polling_interval' => 2000, // milliseconds

    // true = getData() returns a Collection, false = plain array
    'as_collection' => true,

    // Max rows allowed in a Signal (null = unlimited)
    'max_rows' => 1000,
];
```

---

Basic Usage
-----------

[](#basic-usage)

### Query Builder

[](#query-builder)

```
use Illuminate\Support\Facades\DB;

$signal = DB::table('orders')
    ->where('status', 'pending')
    ->orderBy('created_at', 'desc')
    ->toSignal();

// $signal is a Signal instance
$signal->getQuery();
// "select * from `orders` where `status` = ? order by `created_at` desc"

$signal->getBindings();
// ["pending"]

$signal->count();
// 3

$signal->getData();
// Illuminate\Support\Collection {
//   0 => { "id": 1, "status": "pending", "total": 120.00, ... },
//   1 => { "id": 2, "status": "pending", "total": 89.50,  ... },
//   2 => { "id": 3, "status": "pending", "total": 45.00,  ... },
// }
```

### Eloquent Builder

[](#eloquent-builder)

```
$signal = Order::query()
    ->with('customer')
    ->where('status', 'pending')
    ->toSignal();

$signal->getModelClass();
// "App\Models\Order"

$signal->first();
// App\Models\Order { #id: 1, #status: "pending", ... }

$signal->pluck('total');
// Illuminate\Support\Collection [120.00, 89.50, 45.00]
```

### Override config per call

[](#override-config-per-call)

```
$signal = Product::active()->toSignal([
    'polling_interval' => 5000,
    'max_rows'         => 50,
]);

$signal->toArray();
// [
//   "data" => [ ... up to 50 products ... ],
//   "meta" => [
//     "count"            => 12,
//     "model_class"      => "App\Models\Product",
//     "polling_interval" => 5000,   //  null,   // null when not paginated
//   ]
// ]
```

---

Using in Livewire
-----------------

[](#using-in-livewire)

Declare a `Signal` as a public property — it serializes/hydrates automatically via the built-in Livewire synthesizer:

```
use Livewire\Component;
use Laravelldone\SqlToSignal\Signal;

class OrderDashboard extends Component
{
    public Signal $orders;

    public function mount(): void
    {
        $this->orders = Order::pending()->toSignal();
    }

    public function refresh(): void
    {
        $this->orders = $this->orders->refresh();
        // Re-runs the original SQL with the same bindings.
        // No need to rebuild the query from scratch.
    }

    public function render()
    {
        return view('livewire.order-dashboard');
    }
}
```

```

    Refresh

    @foreach ($orders->getData() as $order)
        {{ $order->id }} — {{ $order->status }}
    @endforeach

    Total: {{ $orders->count() }}

```

**What Livewire sends over the wire** (dehydrated payload):

```
{
    "data":            [{ "id": 1, "status": "pending" }, ...],
    "query":           "select * from `orders` where `status` = ?",
    "bindings":        ["pending"],
    "model_class":     "App\\Models\\Order",
    "connection_name": "mysql",
    "config":          { "polling_interval": 2000, "max_rows": 1000 },
    "pagination_meta": null
}
```

For a paginated Signal, `pagination_meta` carries the full page state:

```
{
    "pagination_meta": {
        "total": 87, "per_page": 15, "current_page": 2,
        "last_page": 6, "from": 16, "to": 30
    }
}
```

On the next request Livewire hydrates this back into a full `Signal` — no database hit until you call `refresh()`.

### Auto-polling with Livewire

[](#auto-polling-with-livewire)

```

    @foreach ($orders->getData() as $order)
        {{ $order->id }} — {{ $order->status }}
    @endforeach

```

Every 5 seconds Livewire calls `refresh()`, re-executes the query, and re-renders only the changed rows.

---

Using with Alpine.js
--------------------

[](#using-with-alpinejs)

`Signal` implements `JsonSerializable`, so you can pass it directly to `@js` or an API endpoint:

```

    Total:

```

`@js($orders)` renders:

```
{
    "data": [
        { "id": 1, "status": "pending", "total": "120.00" },
        { "id": 2, "status": "pending", "total": "89.50"  },
        { "id": 3, "status": "pending", "total": "45.00"  }
    ],
    "meta": {
        "count":            3,
        "model_class":      "App\\Models\\Order",
        "polling_interval": 2000
    }
}
```

Use `signal.meta.polling_interval` to drive a JS polling interval without hard-coding it:

```
setInterval(() => fetch('/orders').then(r => r.json()).then(d => signal = d),
            signal.meta.polling_interval);
```

---

Pagination
----------

[](#pagination)

Unlike Livewire's built-in `WithPagination` (which only works inside `render()`), Signal pagination works anywhere — including `mount()`.

```
public function mount(): void
{
    $this->orders = Order::pending()
        ->orderBy('created_at', 'desc')
        ->toSignal(['per_page' => 15, 'page' => 1]);
}

public function nextPage(): void { $this->orders = $this->orders->nextPage(); }
public function prevPage(): void { $this->orders = $this->orders->prevPage(); }
public function goToPage(int $page): void { $this->orders = $this->orders->goToPage($page); }
```

```
@foreach ($orders->getData() as $order)
    {{ $order->id }} — {{ $order->status }}
@endforeach

    Page {{ $orders->getCurrentPage() }} of {{ $orders->getLastPage() }}
    &nbsp;·&nbsp; {{ $orders->getTotal() }} total

getCurrentPage() === 1)>← Prev
getCurrentPage() === $orders->getLastPage())>Next →
```

Pagination meta is carried in `toArray()` and survives the Livewire wire round-trip:

```
{
    "data": [ ... ],
    "meta": {
        "count": 15,
        "polling_interval": 2000,
        "pagination": {
            "total": 87,
            "per_page": 15,
            "current_page": 1,
            "last_page": 6,
            "from": 1,
            "to": 15
        }
    }
}
```

Pass it to Alpine.js for client-side pagination controls without any extra wiring:

```

```

### Pagination API

[](#pagination-api)

MethodReturn typeDescription`isPaginated()``bool``true` when created with `per_page``getTotal()``int`Total rows across all pages`getPerPage()``int`Rows per page`getCurrentPage()``int`Current page number`getLastPage()``int`Last page number`nextPage()``Signal`Signal for the next page (clamped at last page)`prevPage()``Signal`Signal for the previous page (clamped at page 1)`goToPage(int $page)``Signal`Signal for an arbitrary page---

Signal API
----------

[](#signal-api)

MethodReturn typeDescription`getData()``Collection`Full result set for the current page`getQuery()``string`Base SQL with `?` placeholders (no LIMIT/OFFSET)`getBindings()``array`Ordered binding values`getModelClass()``string|null`Eloquent model class, or `null` for raw queries`getConnectionName()``string|null`Database connection name`refresh()``Signal`Re-runs the query; re-runs the same page if paginated`count()``int`Row count for the current page`isEmpty()``bool``true` when the current page is empty`first()``mixed`First row/model on the current page, or `null``pluck(key, value?)``Collection`Delegates to `Collection::pluck()``toArray()``array``['data' => [...], 'meta' => [...]]``toLivewire()``array`Full serialized payload for Livewire transport`Signal::fromLivewire($value)``Signal`Reconstructs a `Signal` from a Livewire payload---

Safety: max\_rows
-----------------

[](#safety-max_rows)

To prevent accidentally serializing large datasets through Livewire's JSON cycle, an `OverflowException` is thrown when the result count exceeds `max_rows`:

```
// Table has 1 500 rows — this throws immediately
$signal = Report::query()->toSignal(['max_rows' => 500]);

// OverflowException: Signal result set exceeds the configured max_rows limit
// of 500. Got 1500 rows.
```

Scope your query before calling `toSignal()`, or set `max_rows` to `null` to disable the limit entirely:

```
// Safe — scoped
$signal = Report::thisMonth()->toSignal(['max_rows' => 500]);

// Unlimited — use with care
$signal = Report::query()->toSignal(['max_rows' => null]);
```

---

[![Support me on Ko-fi](https://camo.githubusercontent.com/ce5236a99602a590f6e988f98173b57c238f3a90b74cb5885a0659a52d8e3548/68747470733a2f2f6b6f2d66692e636f6d)](https://ko-fi.com)

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance84

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

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 ~0 days

Total

5

Last Release

97d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/33a01c3423be5158fc89f506a57bb31743896332069195d614e205ee4d42a073?d=identicon)[neon2027](/maintainers/neon2027)

---

Top Contributors

[![neon2027](https://avatars.githubusercontent.com/u/107676904?v=4)](https://github.com/neon2027 "neon2027 (11 commits)")

---

Tags

laravelsignaleloquentlivewirealpinejsreactive

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/laravelldone-sql-to-signal/health.svg)

```
[![Health](https://phpackages.com/badges/laravelldone-sql-to-signal/health.svg)](https://phpackages.com/packages/laravelldone-sql-to-signal)
```

###  Alternatives

[spatie/laravel-medialibrary

Associate files with Eloquent models

6.1k41.3M596](/packages/spatie-laravel-medialibrary)[kirschbaum-development/eloquent-power-joins

The Laravel magic applied to joins.

1.6k29.9M42](/packages/kirschbaum-development-eloquent-power-joins)[spatie/laravel-health

Monitor the health of a Laravel application

87411.3M152](/packages/spatie-laravel-health)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[watson/validating

Eloquent model validating trait.

9733.4M53](/packages/watson-validating)[yajra/laravel-oci8

Oracle DB driver for Laravel via OCI8

8723.1M23](/packages/yajra-laravel-oci8)

PHPackages © 2026

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