You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We've all been there. You start with a simple observer to send an email when a user's status changes. Five years later, that observer has become more than that, handling notifications, audit logs, cache invalidation or whatever else you could think of - all tangled together in updated().
I've seen it happen countless times. Observers are a great concept, but in practice they often become a dumping ground for any logic related to a model's lifecycle. It's why I actually avoid using them in my projects. The lack of structure and visibility leads to maintenance headaches down the line.
Common pain points:
Observer bloat - Single responsibility goes out the window
No native testing support - You can't fake() an observer like you can notifications or events
Hidden side effects - Observers are registered in providers, invisible when reading the model
Not queueable - Have to manually dispatch jobs from within the observer
All-or-nothing - Fires on every save, requiring manual wasChanged() checks
Boilerplate-heavy - Separate class, provider registration, manual wiring
Before or after save - Control timing with the Timing enum:
// Validate before the save happens (can throw to prevent it)
#[Watch('status', ValidateStatusTransition::class, Timing::SAVING)]
// React after the save completes (default)
#[Watch('status', SendStatusNotification::class, Timing::SAVED)]
Multi-column watching - One handler for related columns:
#[Watch(['billing_address', 'shipping_address'], RecalculateTax::class)]
class Order extends Model {}
Global enable/disable - Perfect for seeders and migrations:
ColumnWatcher::disable();
// Seed 10,000 users without triggering watchersUser::factory()->count(10000)->create();
ColumnWatcher::enable();
Infinite loop protection - Safely modify the model within a handler without re-triggering.
Octane compatible - Scoped state bindings prevent cross-request contamination.
Comparison
Aspect
Observers
Column Watcher
Responsibility
One class, many concerns
One class, one concern
Testing
Manual mocking
Built-in fake()
Queueing
Dispatch jobs manually
implements ShouldQueue
Granularity
All model events
Specific columns
Change detection
Manual wasChanged()
Automatic
Looking for Feedback
I'd love to hear the community's thoughts on:
API design - Does the attribute-based approach feel natural?
Feature gaps - What's missing that you'd need for your use cases?
Edge cases - Any scenarios where observers still win? For me, the thinking would change where observers are responsible for reacting to any change on the model, while column watchers are only responsible for specific columns. So if you have logic that needs to run on every update, regardless of which fields changed, observers are still the way to go.
Naming - Is "Column Watcher" clear, or would another name resonate better?
Would you like to see this in Laravel core?
I guess you could argue this is a DSL for a common event pattern, but I feel this shines when:
You have many column-specific reactions across many models
You want the "what triggers what" visible on the model itself
You want to fake/assert individual handlers in tests without faking all events
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The Problem with Observers
We've all been there. You start with a simple observer to send an email when a user's status changes. Five years later, that observer has become more than that, handling notifications, audit logs, cache invalidation or whatever else you could think of - all tangled together in updated().
I've seen it happen countless times. Observers are a great concept, but in practice they often become a dumping ground for any logic related to a model's lifecycle. It's why I actually avoid using them in my projects. The lack of structure and visibility leads to maintenance headaches down the line.
Common pain points:
Introducing Column Watcher
See here:
https://github.com/CWAscend/laravel-column-watcher
Feel free to read the README and skip the rest of this, it's more extensive.
What if you could declare watchers directly on your model, with built-in queueing, testing, and single-responsibility handlers?
Each handler is a focused, single-purpose class:
Key Features
Visible declarations - Watchers are declared on the model, not hidden in a service provider (ok, we now have
#[ObservedBy()])Queueable out of the box - Just implement ShouldQueue for that async goodness:
Built-in test faking - Like notifications, events, and queues:
Before or after save - Control timing with the Timing enum:
Multi-column watching - One handler for related columns:
Global enable/disable - Perfect for seeders and migrations:
Infinite loop protection - Safely modify the model within a handler without re-triggering.
Octane compatible - Scoped state bindings prevent cross-request contamination.
Comparison
fake()implements ShouldQueuewasChanged()Looking for Feedback
I'd love to hear the community's thoughts on:
I guess you could argue this is a DSL for a common event pattern, but I feel this shines when:
Thanks in advance!
All reactions