PHP code example of ascend / laravel-column-watcher
1. Go to this page and download the library: Download ascend/laravel-column-watcher library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
ascend / laravel-column-watcher example snippets
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
}
}
// 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)
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);
}
}
}
// 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
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
}
class SyncToExternalApi extends ColumnWatcher implements ShouldQueue
{
protected function execute(ColumnChange $change): void
{
// Runs in the background automatically
}
}
namespace App\Watchers;
use Ascend\LaravelColumnWatcher\ColumnWatcher;
use Ascend\LaravelColumnWatcher\Data\ColumnChange;
class HandleStatusChange extends ColumnWatcher
{
protected function execute(ColumnChange $change): void
{
// $change->model - The model instance
// $change->column - The column that changed ('status')
// $change->oldValue - The previous value
// $change->newValue - The new value
}
}
namespace App\Models;
use Ascend\LaravelColumnWatcher\Attributes\Watch;
use App\Watchers\HandleStatusChange;
use Illuminate\Database\Eloquent\Model;
#[Watch('status', HandleStatusChange::class)]
class Request extends Model
{
// ...
}
use Ascend\LaravelColumnWatcher\ColumnWatcher;
use App\Models\Request;
use App\Watchers\HandleStatusChange;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
ColumnWatcher::register(Request::class, 'status', HandleStatusChange::class);
}
}
#[Watch(['name', 'email', 'phone'], HandleContactInfoChange::class)]
class User extends Model {}
#[Watch('status', HandleStatusChange::class)]
#[Watch('status', NotifyAdmins::class)]
#[Watch('priority', HandlePriorityChange::class)]
class Request extends Model {}
use Ascend\LaravelColumnWatcher\Enums\Timing;
// Runs AFTER save (default) - use for notifications, logging, side effects
#[Watch('status', SendNotification::class)]
// Runs BEFORE save - use for validation, transformation, blocking saves
#[Watch('status', ValidateStatusTransition::class, Timing::SAVING)]
use Ascend\LaravelColumnWatcher\ColumnWatcher;
use Ascend\LaravelColumnWatcher\Data\ColumnChange;
use Illuminate\Contracts\Queue\ShouldQueue;
class SyncToExternalService extends ColumnWatcher implements ShouldQueue
{
protected function execute(ColumnChange $change): void
{
// This runs in the background via queue worker
ExternalApi::sync($change->model);
}
}
class SyncToExternalService extends ColumnWatcher implements ShouldQueue
{
public $connection = 'redis';
public $queue = 'external-sync';
public $tries = 3;
public $timeout = 30;
protected function execute(ColumnChange $change): void
{
// ...
}
}
class HandleStatusChange extends ColumnWatcher
{
protected function execute(ColumnChange $change): void
{
// Properties
$change->model; // The Eloquent model instance
$change->column; // The column name that changed (string)
$change->oldValue; // The previous value (mixed)
$change->newValue; // The new value (mixed)
// Helper methods
$change->hasChanged(); // true if oldValue !== newValue
$change->wasNull(); // true if oldValue was null
$change->isNull(); // true if newValue is null
$change->wasEmpty(); // true if oldValue was empty
$change->isEmpty(); // true if newValue is empty
}
}
#[Watch('status', HandleRequestStatusChange::class)]
class Request extends Model {}
#[Watch('status', ValidateStatusTransition::class, Timing::SAVING)]
class Order extends Model {}
class ValidateStatusTransition extends ColumnWatcher
{
private array $allowedTransitions = [
'pending' => ['processing', 'cancelled'],
'processing' => ['shipped', 'cancelled'],
'shipped' => ['delivered'],
];
protected function execute(ColumnChange $change): void
{
$allowed = $this->allowedTransitions[$change->oldValue] ?? [];
if (!in_array($change->newValue, $allowed)) {
throw new InvalidStatusTransition(
"Cannot transition from {$change->oldValue} to {$change->newValue}"
);
}
}
}
#[Watch(['name', 'email', 'plan'], SyncToStripe::class)]
class Customer extends Model {}
class SyncToStripe extends ColumnWatcher implements ShouldQueue
{
public $queue = 'integrations';
protected function execute(ColumnChange $change): void
{
$customer = $change->model;
// Resolve dependencies in execute(), not the constructor
// Constructor injection doesn't work with queued jobs
$stripe = app(StripeClient::class);
$stripe->customers->update($customer->stripe_id, [
'name' => $customer->name,
'email' => $customer->email,
'metadata' => ['plan' => $customer->plan],
]);
}
}
// app/Observers/RequestObserver.php
class RequestObserver
{
public function saved(Request $request): void
{
if ($request->wasChanged('status')) {
HandleStatusChange::dispatch($request);
}
if ($request->wasChanged('priority')) {
HandlePriorityChange::dispatch($request);
}
if ($request->wasChanged(['name', 'description'])) {
HandleMetadataChange::dispatch($request);
}
}
}
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Request::observe(RequestObserver::class);
}
// app/Models/Request.php
#[Watch('status', HandleStatusChange::class)]
#[Watch('priority', HandlePriorityChange::class)]
#[Watch(['name', 'description'], HandleMetadataChange::class)]
class Request extends Model {}
use Ascend\LaravelColumnWatcher\ColumnWatcher;
// Disable all watchers
ColumnWatcher::disable();
// Run operations without triggering watchers
$request->update(['status' => 'archived']);
// Re-enable watchers
ColumnWatcher::enable();
// Check if enabled
if (ColumnWatcher::isEnabled()) {
// ...
}
use Ascend\LaravelColumnWatcher\Events\WatcherStarted;
use Ascend\LaravelColumnWatcher\Events\WatcherSucceeded;
use Ascend\LaravelColumnWatcher\Events\WatcherFailed;
// In a service provider or listener
Event::listen(WatcherStarted::class, function (WatcherStarted $event) {
Log::debug('Watcher starting', [
'watcher' => get_class($event->watcher),
'model' => get_class($event->watcher->model),
'model_id' => $event->watcher->model->getKey(),
'column' => $event->watcher->column,
'old_value' => $event->watcher->oldValue,
'new_value' => $event->watcher->newValue,
]);
});
Event::listen(WatcherSucceeded::class, function (WatcherSucceeded $event) {
Metrics::increment('watcher.success', [
'watcher' => get_class($event->watcher),
]);
});
Event::listen(WatcherFailed::class, function (WatcherFailed $event) {
Log::error('Watcher failed', [
'watcher' => get_class($event->watcher),
'exception' => $event->exception->getMessage(),
]);
// Report to error tracking service
report($event->exception);
});
// config/column-watcher.php
return [
// Globally enable/disable column watching
'enabled' => env('COLUMN_WATCHER_ENABLED', true),
// Default namespace for generated watchers
'namespace' => 'App\\Watchers',
// Directories to scan for models (used by watcher:list)
'model_paths' => ['app/Models'],
];
namespace App\Watchers;
use Ascend\LaravelColumnWatcher\ColumnWatcher;
use Ascend\LaravelColumnWatcher\Data\ColumnChange;
class HandleStatusChange extends ColumnWatcher
{
protected function execute(ColumnChange $change): void
{
//
}
}
use Illuminate\Contracts\Queue\ShouldQueue;
class SyncToExternalService extends ColumnWatcher implements ShouldQueue
{
protected function execute(ColumnChange $change): void
{
//
}
}
use App\Watchers\HandleStatusChange;
public function test_status_change_triggers_handler(): void
{
HandleStatusChange::fake();
$request = Request::factory()->create(['status' => 'draft']);
$request->update(['status' => 'submitted']);
// Assert the handler was triggered
HandleStatusChange::assertTriggered();
// Assert with specific conditions
HandleStatusChange::assertTriggered(
fn ($change) => $change->newValue === 'submitted'
);
// Assert specific values
HandleStatusChange::assertTriggeredWithValues('draft', 'submitted');
// Assert for a specific column
HandleStatusChange::assertTriggeredForColumn('status');
// Assert triggered exactly N times
HandleStatusChange::assertTriggeredTimes(1);
}
public function test_handler_not_triggered_when_column_unchanged(): void
{
HandleStatusChange::fake();
$request = Request::factory()->create(['status' => 'draft']);
$request->update(['name' => 'New Name']); // status not changed
HandleStatusChange::assertNotTriggered();
}
HandleStatusChange::fake();
$request->update(['status' => 'submitted']);
$request->update(['status' => 'approved']);
$changes = HandleStatusChange::recorded();
// $changes is an array of ColumnChange objects
$this->assertCount(2, $changes);
$this->assertEquals('submitted', $changes[0]->newValue);
$this->assertEquals('approved', $changes[1]->newValue);
use Ascend\LaravelColumnWatcher\ColumnWatcher;
ColumnWatcher::disable();
// Watchers won't fire for any operations
$request->update(['status' => 'archived']);
ColumnWatcher::enable();
public function test_status_change_sends_notification(): void
{
Notification::fake();
$request = Request::factory()->create(['status' => 'draft']);
$request->update(['status' => 'submitted']);
Notification::assertSentTo($request->submitter, RequestSubmitted::class);
}
protected function tearDown(): void
{
HandleStatusChange::stopFaking();
parent::tearDown();
}
use Ascend\LaravelColumnWatcher\ColumnWatcher;
protected function setUp(): void
{
parent::setUp();
ColumnWatcher::withoutAfterCommit();
}
public function test_status_change_sends_notification(): void
{
Notification::fake();
$request = Request::factory()->create(['status' => 'draft']);
$request->update(['status' => 'submitted']);
// This now works with DatabaseTransactions
Notification::assertSentTo($request->submitter, RequestSubmitted::class);
}
class StatusWatcher extends ColumnWatcher
{
protected function execute(ColumnChange $change): void
{
// This save won't trigger StatusWatcher again for the same column
$change->model->updated_at = now();
$change->model->save();
}
}
DB::transaction(function () {
$order->status = 'approved';
$order->save(); // Handler queued but not dispatched yet
throw new Exception('Oops!'); // Transaction rolls back
});
// Queue job is never dispatched - correct behavior!
class SyncToExternal extends ColumnWatcher implements ShouldQueue
{
protected function execute(ColumnChange $change): void
{
// Model is guaranteed to exist here because SerializesModels
// throws ModelNotFoundException before execute() is called
// if the model was deleted
}
public function failed(\Throwable $exception): void
{
if ($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
// Model was deleted, handle gracefully
return;
}
throw $exception;
}
}
use Ascend\LaravelColumnWatcher\Enums\Timing;
// This will throw InvalidTimingException
#[Watch('status', QueueableHandler::class, Timing::SAVING)]
class Order extends Model {}
bash
php artisan make:watcher HandleStatusChange
# For queueable handlers (runs in background):
php artisan make:watcher SyncToExternalService --queued