Column mapping
After upload, every column gets a suggested field. A complete suggestion — every required field, nothing fuzzy — is applied at upload (status mapped); otherwise the session waits for you (needs_mapping).
How columns are suggested
Headers are normalised: ASCII-folded, lower-cased, a trailing * / (required) / (povinné) dropped, everything else non-alphanumeric turned into _ (Inventárne č. * → inventarne_c). Each column then gets the best of:
| Match | Confidence | Source |
|---|---|---|
| a saved mapping for this exact header row | 1.00 | saved |
| the field name | 1.00 | name |
| a label in headers.locales, the current or the fallback locale | 0.98 | label |
| an alias | 0.95 | alias |
| a fuzzy match ≥ headers.fuzzy_threshold — flagged, never applied without review | ≥ 0.85 | fuzzy |
Greedy, each field once, ties to the leftmost column.
$suggestion = $session->suggestMapping();
$suggestion->columns; // list<SuggestedColumn>: index, header, field, confidence, source; isFuzzy()
$suggestion->unmappedRequired; // field names
$suggestion->ignoredColumns; // column indexes
$suggestion->savedMapping; // the saved mapping's name, or null
$suggestion->isComplete();
$session->map($suggestion->toMapping()); // apply it as it isMap by header or column
use RoundlyConsulting\Imports\DataTransferObjects\ColumnMapping;
$session->map(['Inventárne č.' => 'asset_tag', 3 => 'name']); // by header text or 0-based column index (3 = column D)
$session->map(['Notes' => null]); // leave a column unmapped
$session->map(ColumnMapping::fromArray([2 => 'name'])); // always by 0-based column index
$session->saveMapping('ERP export');- Integer keys are 0-based column indexes — unless a header reads that number (a header 2024 becomes an integer key in PHP), in which case it maps by its text.
- A number that is a header and the index of a different column is refused rather than guessed — map it explicitly with ColumnMapping, which is always by index.
- Refused with InvalidMappingException (422, errors keyed columns.<column> / fields.<field>): an unknown field or column, a field mapped twice, a duplicated header named by text (use its number), a required field left unmapped.
- Changing the mapping drops the preview and bumps preview_version; a commit against an older version is refused.
- Once a commit holds the session — even one started a moment earlier from another tab or worker — a mapping change is refused with ImportStateException, so it never rewrites a running commit’s plan.
Saved mappings
Name a mapping once and every later file with the same headers maps itself — even with the columns in a different order. Save it from a session or from header text:
$mappings = Imports::for($team)->by($user)->mappings('assets');
$mappings->all(); // list<SavedMapping>
$mappings->find('ERP export'); // ?SavedMapping
$mappings->save('ERP export', ['Inventárne č.' => 'asset_tag', 'Názov' => 'name']);
$mappings->delete('ERP export'); // false when there was none- Both $session->saveMapping($name) (the session’s headers and mapping, plus its delimiter, sheet and header row) and mappings($importer)->save() run SaveMappingAction, so both store the same shape, fire MappingSaved and are recorded alike by the fake. Saving under an existing name replaces it.
- The action checks, in order: saved mappings are on; a row importer; authorize(Upload); a 1–60 character name, at least one column, every field known and mapped once; then the per-importer and size caps. Refusals throw InvalidSavedMappingException or TooManySavedMappingsException (both 422).
- Stored per owner through options-for-laravel (the imports.mappings option, one row per owner), keyed by normalised header, and auto-applied to an identical header row — never when normalised headers collide.
- Header signatures are locale-independent — always transliterated under en — so a mapping saved by a Slovak user is recognised for a German one.
- Up to mappings.max_per_importer (20) per importer and mappings.max_bytes in all. Writes are serialised with a cache lock — set mappings.lock_store to a shared store.
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 cryptoBy 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.