NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Imports for Laravel

Commit and progress

use RoundlyConsulting\Imports\Enums\RecordAction;

$batch = $session->commit();     // BatchHandle
$batch->status(); $batch->progress(); $batch->summary(); $batch->records(RecordAction::Created);
$batch->isRunning(); $batch->isFinished(); $batch->isRolledBack();   // each re-reads the row
$batch->cancel(); $batch->resume();

How a commit runs

  • authorize(Commit); the session must be previewed with the caller’s preview version and not expired (409 otherwise).
  • The lock moves the session to committing; an ImportBatch row records the committer.
  • One Laravel job batch holding one sequential chain of chunk jobs on imports.queue.* (sync with the queue off). Chunk size = max(commit.chunk_size, ceil(rows / commit.max_chunks)), fixed at preview. ImportStarted fires just before the dispatch; if the queue refuses it, the commit fails at once (dispatch_failed), the lock is released and the exception rethrown.
  • Each chunk runs inside the owner’s context. The first chunk of each run re-asks authorize(Commit) for the committer — a user who lost access fails the batch (unauthorized).
  • One transaction per chunk. It locks the batch row and checks that the batch is running and the chunk is next, so a duplicate delivery, a retry of a committed chunk or any job of a stopped batch does nothing.
  • Invalid and duplicate rows are skipped; every other row is re-validated and re-matched against current data (reclassified when that changed). Each row runs in a savepoint: your callback, then checks — the record is saved, on the import connection and inside the owner.
  • Your writes, the ledger, the row results and the chunk counter commit or roll back together. About one query per row: ledger rows are bulk-inserted per chunk, so the ledger model’s own Eloquent events do not fire — listen to ImportChunkProcessed or ImportCompleted.
  • Finished → completed (ImportCompleted); failed → failed (ImportFailed, resumable). Both run inside the owner’s context, set finished_at and undo_until, and release the lock.

Exceptions in your callbacks

  • ImportRowException and ValidationException fail the row with their message.
  • A unique index violation in your table fails the row as duplicate_created_meanwhile.
  • Any other exception fails the row with a generic message and is reported.
  • A database concurrency or lost-connection error, and any non-Exception throwable, is rethrown: the chunk rolls back and the job retries (commit.tries, commit.backoff, commit.timeout).

Side effects

Mail, jobs and files written by callbacks are outside the transaction. Dispatch them after commit, so a rolled-back chunk never sends anything:

public function create(ImportRow $row, ImportContext $context): Model
{
    $member = Member::create([...$row->attributes(), ...$context->ownerAttributes()]);

    SendWelcome::dispatch($member)->afterCommit();   // never sent for a chunk that rolls back

    return $member;
}

Cancel and resume

cancel() stops the chain — rows already written stay and can be undone. resume() starts a new batch from the next chunk for a failed or cancelled run, refused once the file or working copy was pruned. ImportSummary: created, linked, updated, skipped, failed, reclassified, duplicates, invalid and durationSeconds.

One running import per owner and importer

A unique lock key on the session allows one running commit or rollback per owner and importer, set by one conditional update that also moves the status. A double-clicked commit gets ImportStateException; another session of the same owner and importer gets ImportAlreadyRunningException (409, naming it). imports:sweep releases a run with no progress for sessions.stale_after whose job batch is finished, cancelled, failing or gone, and any run silent for sessions.abandon_after — a run that is merely slow is left alone. A run whose owner was deleted is still failed and its lock released, but no event fires — there is no owner to name. An error on one session is reported and the sweep carries on with the others.

The ledger: “imported” on every record

imported_records holds one row per record a commit created, linked or updated — the polymorphic “imported from” pivot. Add HasImportSource to a model to read it:

use RoundlyConsulting\Imports\Concerns\HasImportSource;

final class Asset extends Model
{
    use HasImportSource;
}

$asset->importRecords;              // MorphMany<ImportedRecord>
$asset->importBatch();              // ?ImportBatch — the latest batch that wrote it whose undo did not remove it
$asset->wasImported();              // bool
Asset::importedIn($batch->model())->get();

The trait is read-only. A record an undo kept still counts as imported.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.