Browse documentationOpen

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;
EventDispatched when
SyncStartedA run moves to running — for queued runs, when the worker picks it up
SyncCompletedA run finishes as completed or completed_with_errors
SyncFailedA 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 records

The 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:

PathEvents dispatched from
sync:run --nowThe command process
Queued, batchedSyncStarted from the file job; SyncCompleted / SyncFailed from the batch callback
Queued, atomic()The file job, inside the file's transaction
sync:retryThe dispatched job
sync:retry --failed-rowsThe 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.