ascend/laravel-column-watcher
Attribute-based “column watchers” for Laravel Eloquent models. Trigger handlers only when specific attributes change (before/after save), with support for multiple watchers, queueable handlers, events, and Octane compatibility—avoiding bulky, monolithic model observers.
Installation:
composer require ascend/laravel-column-watcher
Publish the config (optional):
php artisan vendor:publish --provider="Ascend\ColumnWatcher\ColumnWatcherServiceProvider"
First Use Case: Watch a single column on a model:
use Ascend\ColumnWatcher\Watch;
class User extends Model
{
#[Watch('email', handler: EmailUpdatedHandler::class)]
public string $email;
}
Run Migrations/Tests: Verify the package works by triggering a column update and checking if the handler executes.
ColumnChange object docs to understand event payloads.config/column-watcher.php for global settings (e.g., default queue connection).Column-Specific Logic:
Replace observer updated() methods with granular handlers:
// Instead of:
protected static function updated(Model $model): void
{
if ($model->isDirty('email')) { ... }
if ($model->isDirty('status')) { ... }
}
// Use:
#[Watch('email', handler: EmailHandler::class)]
#[Watch('status', handler: StatusHandler::class)]
Timing Strategies:
#[Watch('price', handler: PriceValidator::class, timing: 'before')]
#[Watch('status', handler: SendNotificationJob::class, timing: 'after', queue: 'high')]
Queueable Handlers: Offload heavy work to queues:
#[Watch('content', handler: ProcessContentJob::class, queue: 'long')]
public string $content;
Dynamic Watchers: Register watchers programmatically (e.g., for polymorphic models):
$model->watch('dynamic_column', DynamicHandler::class);
Leverage ColumnChange:
Access old/new values, model instance, and metadata:
public function handle(ColumnChange $change): void
{
$change->model; // Updated model
$change->column; // 'email'
$change->oldValue; // Previous value
$change->newValue; // New value
}
Combine with Events: Use watchers for side effects and events for broader reactivity:
#[Watch('published_at', handler: ArchiveOldContentJob::class)]
public ?Carbon $published_at;
// In model:
protected $dispatchesEvents = [
'updated' => [ContentUpdated::class],
];
Testing:
Mock handlers or use ColumnWatcher::assertHandled():
$this->assertHandled(User::class, 'email', EmailUpdatedHandler::class);
Infinite Loop Protection:
ignoreLoopProtection sparingly or implement custom logic:
#[Watch('recursive_field', handler: RecursiveHandler::class, ignoreLoopProtection: true)]
Timing Conflicts:
before handlers run before model events (e.g., saving).after handlers run after saved but before updated.Model::fireModelEvent('saving') in before handlers to trigger custom events.Queue Delays:
ColumnChange object to the job for consistency.Mass Assignment:
$model->email = 'test').fill() or update() may bypass watchers if columns aren’t in $fillable.#[Watch(..., trigger: 'always')] or override fill():
public function fill(array $attributes = []): static
{
$this->fireColumnWatchersBeforeFill($attributes);
return parent::fill($attributes);
}
debug: true in config/column-watcher.php to log handler execution.ColumnWatcher::handlers() to list registered watchers:
dd(ColumnWatcher::handlers(User::class));
ColumnChange:
Manually trigger watchers in tests:
$change = new ColumnChange(User::class, 'email', 'old@example.com', 'new@example.com');
$handler = app(EmailUpdatedHandler::class);
$handler->handle($change);
Custom Handlers:
Implement Ascend\ColumnWatcher\Contracts\Handler for reusable logic:
class LogColumnChange implements Handler
{
public function handle(ColumnChange $change): void
{
Log::info("Column {$change->column} changed", ['model' => $change->model]);
}
}
Dynamic Conditions:
Use #[Watch(..., condition: fn($old, $new) => ...)] for conditional triggers:
#[Watch('status', handler: NotifyAdminJob::class, condition: fn($old, $new) => $new === 'banned')]
Global Watchers: Register watchers for all models via service provider:
ColumnWatcher::watchAllModels('global_column', GlobalHandler::class);
Override Defaults:
Extend the ColumnWatcher facade to add custom methods:
// app/Providers/ColumnWatcherServiceProvider.php
ColumnWatcher::extend(function ($watcher) {
$watcher->addMacro('log', function ($column) {
$this->watch($column, LogColumnChange::class);
});
});
Usage:
$model->log('audit_column');
How can I help you explore Laravel packages today?