byrcsc/laravel-data-sync · 1.x
Events and listeners.
The three lifecycle events, when each is dispatched, why they are never queued, and how to write a listener that does not slow a sync down.
Three events carry the SyncRun model:
use ByRcsc\LaravelDataSync\Events\SyncCompleted;
use ByRcsc\LaravelDataSync\Events\SyncFailed;
use ByRcsc\LaravelDataSync\Events\SyncStarted;| Event | Dispatched when |
|---|---|
SyncStarted | A run moves to running — for queued runs, when the worker picks it up |
SyncCompleted | A run finishes as completed or completed_with_errors |
SyncFailed | A run finishes as failed or cancelled |
All three expose the run:
$event->run->ulid;
$event->run->definition;
$event->run->status;
$event->run->dry_run;
$event->run->write_path;
$event->run->rows_created;
$event->run->rows_failed;
$event->run->error_summary;
$event->run->archive_path;
$event->run->file; // the ledger row
$event->run->failures; // SyncRowFailure recordsThe run passed to SyncCompleted and SyncFailed is reloaded after the final
counters are written, so the numbers on it are the finished ones.
Package events are never queued
None of these events implement ShouldQueue. They are dispatched
synchronously, inside the job or command doing the work, so a listener that
calls a slow API blocks the sync — and on the queued path, blocks the batch's
completion callback.
Queue the listener, not the event:
namespace App\Listeners;
use ByRcsc\LaravelDataSync\Events\SyncCompleted;
use Illuminate\Contracts\Queue\ShouldQueue;
final class NotifyMerchandising implements ShouldQueue
{
public string $queue = 'notifications';
public function handle(SyncCompleted $event): void
{
if ($event->run->dry_run || $event->run->rows_failed === 0) {
return;
}
// notify, open a ticket, post to Slack ...
}
}Register it as you would any listener. The events serialize their models, so a queued listener receives the run by key and reloads it — it sees the persisted record, not a stale in-memory snapshot.
Dry runs dispatch events too
A --dry-run records a run and dispatches SyncStarted and SyncCompleted
like any other. That is useful for testing a listener, and a trap for one that
notifies people. Guard with $event->run->dry_run.
What runs where
Understanding where a listener executes explains which failures it can cause:
| Path | Events dispatched from |
|---|---|
sync:run --now | The command process |
| Queued, batched | SyncStarted from the file job; SyncCompleted / SyncFailed from the batch callback |
Queued, atomic() | The file job, inside the file's transaction |
sync:retry | The dispatched job |
sync:retry --failed-rows | The command process |
For an atomic() definition the completion event fires inside the file's
transaction, so a synchronous listener that writes to the database is part of
that transaction and rolls back with it. A queued listener is not, and may run
before the transaction commits — dispatch it with afterCommit if it reads what
the run wrote.
Follow-up work belongs here
The bulk write path dispatches
no model events, which is exactly why it is fast. When a fast definition still
needs per-import follow-up — reindexing a search collection, warming a cache,
recalculating aggregates — put it in a queued SyncCompleted listener keyed on
the definition:
public function handle(SyncCompleted $event): void
{
if ($event->run->definition !== 'products' || $event->run->dry_run) {
return;
}
ReindexProducts::dispatch();
}That keeps the bulk path intact rather than paying per-row Eloquent costs for work that only needs to happen once per file.
What to read next
- Matching and writing for why model events are absent on the bulk path.
- Run history and status for the same data after the fact.
- Failures and retries for what a
SyncFailedlistener should probably do.