Download the PHP package ascend/laravel-column-watcher without Composer
On this page you can find all versions of the php package ascend/laravel-column-watcher. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download ascend/laravel-column-watcher
More information about ascend/laravel-column-watcher
Files in ascend/laravel-column-watcher
Package laravel-column-watcher
Short Description 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!
License MIT
Informations about the package laravel-column-watcher
Laravel Column Watcher
A Laravel package that provides attribute-based column watching for Eloquent models. React to specific column changes without the boilerplate of full model observers.
Table of Contents
- The Problem
- The Solution
- Installation
- Usage
- Create a Handler
- Register the Watcher
- Watch Multiple Columns
- Multiple Watchers
- Timing: Before vs After Save
- Queueable Handlers
- The ColumnChange Object
- Real-World Examples
- Comparison with Observers
- Disabling Watchers
- Events
- Laravel Octane Compatibility
- Configuration
- Artisan Commands
- Testing
- Cleaning Up Fake State
- Testing Queued Handlers with DatabaseTransactions
- Edge Cases & Limitations
- Requirements
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
Observers tend to grow into monolithic classes that handle everything:
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
Testing observer behaviour is notoriously difficult:
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
When your observer needs to call a slow external API, you're stuck dispatching a job from within the observer:
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
Observers create invisible dependencies:
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
- 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
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
Each handler is a focused, testable, optionally-queueable class.
Fully mockable and fakeable:
Queueable with a single interface:
No observer class. No service provider registration. No manual change detection. The handler only runs when status actually changes.
Installation
The package auto-discovers its service provider. No manual registration needed.
Usage
1. Create a Handler
Generate a handler using the artisan command:
This creates app/Watchers/HandleStatusChange.php:
All handlers extend ColumnWatcher, which includes built-in testing utilities like HandleStatusChange::fake() and HandleStatusChange::assertTriggered(). See Testing for details.
2. Register the Watcher
Option A: Using Attributes (Recommended)
Add the #[Watch] attribute to your model class:
Option B: Programmatic Registration
Register watchers in a service provider:
3. Watch Multiple Columns
A single handler can watch multiple columns:
When any of these columns change, the handler receives a ColumnChange object that tells you which specific column triggered it:
4. Multiple Watchers
You can attach multiple watchers to the same model:
5. Timing: Before vs After Save
By default, handlers run after the model is saved (Timing::SAVED). You can run them before save using the timing parameter:
When to use each:
| Timing | Event | Use Case |
|---|---|---|
Timing::SAVED (default) |
After database write | Notifications, logging, external API calls |
Timing::SAVING |
Before database write | Validation, data transformation, blocking invalid changes |
6. Queueable Handlers
For handlers that perform slow operations (sending emails, calling APIs), add implements ShouldQueue to run them in the background:
You can optionally configure queue behaviour using standard Laravel properties:
Note: Queueable handlers only work with timing: Timing::SAVED (the default). Using timing: Timing::SAVING with a queueable handler would defeat the purpose since the save hasn't happened yet.
The ColumnChange Object
Handlers receive a ColumnChange object with these properties and methods:
Real-World Examples
Send Notification on Status Change
Audit Trail for Sensitive Fields
Validate State Transitions
Sync to External Service (Queued)
Important: Don't use constructor dependency injection in queueable handlers. Laravel serializes the job for the queue and cannot restore injected dependencies. Use app() or resolve() inside the execute() method instead.
Comparison with Observers
| Feature | Observers | Column Watcher |
|---|---|---|
| Testing | Cannot be mocked or faked | Built-in fake(), assertTriggered() |
| Queueing | Must dispatch separate jobs | implements ShouldQueue on handler |
| Code organisation | Tends toward "God observer" | One handler per concern |
| Declaration | Separate class + service provider | Attribute on model |
| Visibility | Hidden in observer class | Visible on model |
| Granularity | All model events | Specific columns only |
| Change detection | Manual (wasChanged()) |
Automatic |
| Multiple handlers | One observer per model | Multiple #[Watch] attributes |
| Timing control | Different methods | Single timing parameter |
Before: Observer Approach
After: Column Watcher Approach
Disabling Watchers
You can temporarily disable watchers globally (useful for migrations, seeding, or testing):
Or use the config:
Events
The package dispatches events during watcher execution, allowing you to hook into the lifecycle for logging, metrics, or error tracking.
Available Events
| Event | Dispatched When |
|---|---|
WatcherStarted |
Before the watcher's execute() method runs |
WatcherSucceeded |
After execute() completes successfully |
WatcherFailed |
When execute() throws an exception |
All events contain a reference to the watcher instance, giving you access to the model, column, and values:
Events Fire for Both Sync and Queued Handlers
Events are dispatched inside the watcher's handle() method, which means they fire:
- Synchronous handlers: Events fire immediately during the model save
- Queued handlers: Events fire when the queue worker processes the job
This gives you consistent behaviour regardless of how the watcher is executed.
Laravel Octane Compatibility
This package is fully compatible with Laravel Octane. Internal state is stored using Laravel's scoped container bindings, which are automatically reset between requests.
You don't need to do anything special - it just works. The package avoids static properties that could cause state to bleed between requests in Octane's long-running worker processes.
Configuration
Publish the config file:
Artisan Commands
watcher:list
List all registered column watchers in your application:
Output (styled similar to Laravel's event:list):
The command scans model directories configured in model_paths and includes programmatically registered watchers. Queueable handlers are marked with [queued].
make:watcher
Generate a new watcher handler class:
This creates a file in app/Watchers/ (configurable via namespace in config):
With --queued, the class also implements ShouldQueue:
Testing
Faking Handlers
All handlers that extend ColumnWatcher can be faked for testing, similar to Laravel's notification and event faking:
Available Assertions
| Method | Description |
|---|---|
Handler::fake() |
Start faking the handler |
Handler::stopFaking() |
Stop faking and clear recorded changes |
Handler::assertTriggered() |
Assert handler was triggered at least once |
Handler::assertTriggered($callback) |
Assert with custom condition |
Handler::assertNotTriggered() |
Assert handler was never triggered |
Handler::assertTriggeredTimes($n) |
Assert handler was triggered exactly N times |
Handler::assertTriggeredForColumn($col) |
Assert handler was triggered for specific column |
Handler::assertTriggeredWithValues($old, $new) |
Assert handler was triggered with specific values |
Handler::recorded() |
Get all recorded ColumnChange objects |
Accessing Recorded Changes
When faking, you can access all recorded changes:
Disabling All Watchers
To disable all watchers globally (useful for migrations, seeding, or bulk operations):
Testing Without Faking
You can also test handler side-effects directly:
Cleaning Up Fake State
When mixing tests that use fake() with tests that need watchers to actually execute, call stopFaking() in your tearDown() to prevent state pollution between tests:
Without this, a test that calls fake() will leave the watcher in fake mode, causing subsequent tests that expect the watcher to run to fail silently.
Testing Queued Handlers with DatabaseTransactions
When using Laravel's DatabaseTransactions trait, queued handlers won't execute because they wait for DB::afterCommit(), which never fires (transactions are rolled back, not committed).
Call withoutAfterCommit() in your setUp() to bypass this:
Note: withoutAfterCommit() is safe to call unconditionally - it only affects behaviour when there's an active transaction.
Edge Cases & Limitations
Model Creation
Watchers fire on both create() and update() operations. When a model is created, the "old value" will be null for all columns:
Infinite Loop Protection
The package automatically prevents infinite loops when a watcher saves the same model:
However, be careful with watchers that modify different columns watched by other handlers.
Transaction Safety
Queued handlers are dispatched after the database transaction commits. If a transaction rolls back, the queued job will not be dispatched:
Deleted Models and Queued Handlers
If a model is deleted before a queued handler processes, Laravel will throw a ModelNotFoundException. Handle this in your handler if needed:
Queueable Handlers Must Use Timing::SAVED
You cannot use queueable handlers with Timing::SAVING. This is enforced at registration time:
Requirements
- PHP 8.2+ (PHP 8.3+ required for Laravel 13)
- Laravel 11, 12, or 13
License
MIT
All versions of laravel-column-watcher with dependencies
illuminate/database Version ^11.0|^12.0|^13.0
illuminate/support Version ^11.0|^12.0|^13.0