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

Validated preview

upload() checks the whole file (format, encoding, limits, header row), stores it and creates an ImportSession. The session holds the source, the mapping, the mode, counts, progress and status — never row values.

Statuses

uploaded → needs_mapping | mapped → validating → previewed → committing → completed → rolling_back → rolled_back
                                      └ preview_failed        └ failed (resumable)      └ rollback_failed
                                                               └ cancelled
any status before the commit → discarded (discard()) | expired (imports:sweep after sessions.expire_after)

Run the preview

use RoundlyConsulting\Imports\DataTransferObjects\RowFilter;

$session->preview();                    // up to preview.sync_threshold data rows: now; more: ValidatePreviewJob
$session->preview(queue: true);         // force the queue (false: force inline)
$session->isValidating();               // still on the queue: poll until…
$session->isPreviewed();                // …every row is checked and the session can be committed
$session->isCommitted();                // committing or past it
$session->progress();                   // Progress(processed, total, percent, phase)
$session->counts();                     // ImportCounts
$session->previewData();                // ImportPreview(counts, updateNotice, mode, unmappedColumns)
$session->rows(RowFilter::invalid())->paginate(50);   // RowResult(rowNumber, status, outcome, errors, match, changes)

Every row goes through the value pipeline, Laravel validation, batched reference lookups and batched natural-key matching, window by window — inside the owner, writing nothing to your tables and calling no importer callback. Up to preview.sync_threshold (500) data rows validate in the request; larger files validate on the queue with progress (ImportPreviewProgressed every preview.progress_every rows).

Counts

  • total = valid + invalid + duplicates; duplicates = matched + duplicatesInFile.
  • creates, updates and skips follow the mode.
  • templateRows (leftover example or hint rows) and blankRows are reported apart.

Row results

use RoundlyConsulting\Imports\DataTransferObjects\RowFilter;
use RoundlyConsulting\Imports\Enums\RowStatus;

$session->rows(RowFilter::invalid())->paginate(50);
$session->rows(RowFilter::updates())->paginate(20);      // with the field diff: [{field, from, to}]
$session->rows(RowFilter::field('email'))->count();      // invalid or failed rows with an error on email
$session->rows(RowFilter::status(RowStatus::Matched))->cursor();

foreach ($session->rows(RowFilter::invalid())->cursor() as $row) {
    foreach ($row->errors as $error) {
        // $error->field, column, columnLetter, code, message (translated), value (≤ 100 characters)
    }
}

Row results are stored for exceptions only — invalid, duplicate, matched and later failed rows — so a clean 10,000-row file stores none. Each holds errors per cell (field, column number and letter, code, translated message, a 100-character excerpt) but no row values. RowFilter::creates() streams the file; RowFilter::updates() adds the field diff, re-read per page. Filters: all(), invalid(), duplicates(), updates(), creates(), skips(), status(...), field($name).

Duplicate modes

RowSkipUpdateCreateNew
validcreatecreatecreate
matched (an existing record)skipupdatecreate
duplicate in the fileskipskipskip
invalidnever writtennever writtennever written
use RoundlyConsulting\Imports\Enums\DuplicateMode;

$session->mode(DuplicateMode::Update);   // on a previewed session: re-planned without re-reading the file
$session->previewData()->updateNotice;   // translated: undo removes created records only, never updates

A mode the importer does not offer throws ImportModeNotSupportedException (422). On a previewed session, a mode change whose preview moved meanwhile is refused as a stale preview (ImportStateException). Whenever updates are planned, updateNotice says that undo removes only created records and never reverts updates.

Changing things while a large file validates

Changing the mapping or the source (sheet, delimiter, header row) drops the preview and bumps preview_version. A queued preview is unique per session and version, and every write of a run lands only while that run’s version is current — so the newer preview always wins, whichever worker finishes first. For XLSX the preview writes a working copy (a normalised CSV of the sheet) and records the offsets commit chunks start at, so commits never re-parse a workbook.

Every change before the commit — source, mapping, mode, preview, discard and expiry — re-reads the session under a row lock and is refused with ImportStateException (409) once a commit has taken it, even one that started a moment earlier from another tab or worker. It never rewrites a running commit’s plan or deletes its file.

A lost validation

imports:sweep fails a validating session whose progress is older than sessions.stale_after and whose job is no longer queued or running — or that is past sessions.abandon_after — as preview_failed, firing ImportPreviewFailed with reason abandoned. A job that turns up late writes nothing. Preview again to retry.

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.