PHPackages                             ascend/laravel-column-watcher - 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. ascend/laravel-column-watcher

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

ascend/laravel-column-watcher
=============================

Watch and react to Eloquent model column changes with PHP 8 attributes. Trigger handlers when specific columns are modified, with support for queued handlers, before/after save timing, and built-in infinite loop protection. Think observers, but column specific!

v1.0.3(3mo ago)15199↑900%MITPHPPHP ^8.2

Since Feb 11Pushed 3mo ago1 watchersCompare

[ Source](https://github.com/CWAscend/laravel-column-watcher)[ Packagist](https://packagist.org/packages/ascend/laravel-column-watcher)[ RSS](/packages/ascend-laravel-column-watcher/feed)WikiDiscussions 1.x Synced 1mo ago

READMEChangelog (4)Dependencies (4)Versions (5)Used By (0)

Laravel Column Watcher
======================

[](#laravel-column-watcher)

[![Latest Version on Packagist](https://camo.githubusercontent.com/f550a82884c5f5040f8e2ae6423a97ed1248b22f29fb9d48745605277a8c663f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f617363656e642f6c61726176656c2d636f6c756d6e2d776174636865722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ascend/laravel-column-watcher)[![Tests](https://camo.githubusercontent.com/283eaa4706dc2d10791c75351eb3d0db0c71559c7846e91e21aad210ece3c253/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d31343825323070617373696e672d627269676874677265656e3f7374796c653d666c61742d737175617265)](tests)[![PHP Version](https://camo.githubusercontent.com/0137db7634f2bfb58931a1788223a4a9136100008a2ee84e758b20b9c6ec21a0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d626c75653f7374796c653d666c61742d737175617265)](composer.json)[![Laravel Version](https://camo.githubusercontent.com/56356c73492b8bace6d504344d6b993555a422e8694215b4e3a0478551973d45/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c61726176656c2d313125323025374325323031322d7265643f7374796c653d666c61742d737175617265)](composer.json)[![License](https://camo.githubusercontent.com/422db9fd40f5831c765cf6530b6750c081b696bd18d904cf89554df98c676277/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e3f7374796c653d666c61742d737175617265)](LICENSE)

A Laravel package that provides attribute-based column watching for Eloquent models. React to specific column changes without the boilerplate of full model observers.

[![Laravel Column Watcher Code Snippet](img/code-snippet.webp "Laravel Column Watcher Code Snippet")](img/code-snippet.webp)

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

[](#table-of-contents)

- [The Problem](#the-problem)
- [The Solution](#the-solution)
- [Installation](#installation)
- [Usage](#usage)
    - [Create a Handler](#1-create-a-handler)
    - [Register the Watcher](#2-register-the-watcher)
    - [Watch Multiple Columns](#3-watch-multiple-columns)
    - [Multiple Watchers](#4-multiple-watchers)
    - [Timing: Before vs After Save](#5-timing-before-vs-after-save)
    - [Queueable Handlers](#6-queueable-handlers)
- [The ColumnChange Object](#the-columnchange-object)
- [Real-World Examples](#real-world-examples)
- [Comparison with Observers](#comparison-with-observers)
- [Disabling Watchers](#disabling-watchers)
- [Events](#events)
- [Laravel Octane Compatibility](#laravel-octane-compatibility)
- [Configuration](#configuration)
- [Artisan Commands](#artisan-commands)
- [Testing](#testing)
    - [Cleaning Up Fake State](#cleaning-up-fake-state)
    - [Testing Queued Handlers with DatabaseTransactions](#testing-queued-handlers-with-databasetransactions)
- [Edge Cases &amp; Limitations](#edge-cases--limitations)
- [Requirements](#requirements)

The Problem
-----------

[](#the-problem)

Laravel's observer pattern is powerful but fundamentally flawed for column-level reactions. What starts as a simple "notify when status changes" requirement quickly becomes a maintenance burden.

### The "God Observer" Anti-Pattern

[](#the-god-observer-anti-pattern)

Observers tend to grow into monolithic classes that handle everything:

```
class RequestObserver
{
    public function saved(Request $request): void
    {
        if ($request->wasChanged('status')) {
            HandleStatusChange::dispatch($request);
            NotifyAdmins::dispatch($request);
            SyncToExternalApi::dispatch($request);
        }

        if ($request->wasChanged('priority')) {
            HandlePriorityChange::dispatch($request);
        }

        if ($request->wasChanged('assigned_to')) {
            NotifyAssignee::dispatch($request);
            AuditAssignmentChange::dispatch($request);
        }

        if ($request->wasChanged(['name', 'description'])) {
            IndexForSearch::dispatch($request);
        }

        // ... and it keeps growing
    }
}
```

This violates the Single Responsibility Principle. Your observer becomes a dumping ground for unrelated concerns, making it difficult to understand, maintain, and test.

### Observers Can't Be Mocked

[](#observers-cant-be-mocked)

Testing observer behaviour is notoriously difficult:

```
// You can't do this:
RequestObserver::fake();

// You're forced to either:
// 1. Test side-effects directly (fragile, slow)
// 2. Disable observers entirely (loses coverage)
// 3. Create elaborate test doubles (complex, brittle)
```

There's no clean way to verify that your observer logic was triggered without testing the downstream effects. This leads to either undertested code or slow, integration-heavy test suites.

### Observers Can't Be Queued

[](#observers-cant-be-queued)

When your observer needs to call a slow external API, you're stuck dispatching a job from within the observer:

```
class RequestObserver
{
    public function saved(Request $request): void
    {
        if ($request->wasChanged('status')) {
            // You can't queue the observer itself
            // You must create and dispatch a separate job
            SyncStatusToExternalApi::dispatch($request);
        }
    }
}
```

This means creating two classes for every async operation: the observer to detect the change, and the job to handle it. The observer becomes nothing more than a dispatcher.

### The Hidden Coupling Problem

[](#the-hidden-coupling-problem)

Observers create invisible dependencies:

```
// Looking at this model, you have no idea what happens when you save it
class Request extends Model
{
    protected $fillable = ['status', 'priority', 'name'];
}

// The observer is registered in a service provider far, far away
// Surprise side-effects await anyone who modifies this model
```

New developers (and future you) must hunt through service providers and observer classes to understand what happens when a model changes.

### Summary of Observer Pain Points

[](#summary-of-observer-pain-points)

- **Untestable**: No mocking, no faking, no assertions
- **Not queueable**: Must dispatch separate jobs for async work
- **God class tendency**: All column logic piles into one file
- **Hidden behaviour**: Registration buried in service providers
- **Boilerplate heavy**: Separate class + registration for simple reactions
- **All-or-nothing**: Fires on every save, requires manual change detection

The Solution
------------

[](#the-solution)

Column Watcher represents a paradigm shift in how you react to model changes. Instead of centralised observers, you get:

- **Fakeble handlers** with built-in test assertions
- **Queueable handlers** that run in the background
- **Single-purpose handlers** that do one thing well
- **Visible declarations** right on your model

```
use Ascend\LaravelColumnWatcher\Attributes\Watch;

#[Watch('status', HandleStatusChange::class)]
#[Watch('status', SyncToExternalApi::class)]
#[Watch('priority', HandlePriorityChange::class)]
class Request extends Model
{
    // Anyone reading this model immediately knows what happens on change
    // Separate concern, separate handler
}
```

Each handler is a focused, testable, optionally-queueable class.

**Fully mockable and fakeable:**

```
HandleStatusChange::fake();

$request->update(['status' => 'approved']);

HandleStatusChange::assertTriggered();
```

**Queueable with a single interface:**

```
class SyncToExternalApi extends ColumnWatcher implements ShouldQueue
{
    protected function execute(ColumnChange $change): void
    {
        // Runs in the background automatically
    }
}
```

No observer class. No service provider registration. No manual change detection. The handler only runs when `status` actually changes.

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

[](#installation)

```
composer require ascend/laravel-column-watcher
```

The package auto-discovers its service provider. No manual registration needed.

Usage
-----

[](#usage)

### 1. Create a Handler

[](#1-create-a-handler)

Generate a handler using the artisan command:

```
php artisan make:watcher HandleStatusChange

# For queueable handlers (runs in background):
php artisan make:watcher SyncToExternalService --queued
```

This creates `app/Watchers/HandleStatusChange.php`:

```
