All packages
Imports for Laravel
Configuration
The package works with zero configuration. Publish config/imports.php only to override:
php artisan vendor:publish --tag="imports-config"Every value is read strictly: a value of the wrong type or outside its range throws InvalidImportsConfigurationException naming the key, and a blank env value (KEY=) takes the default.
- Durations are ISO-8601 (P7D, PT30M) and must be positive.
- Switches accept any boolean, including on and off.
- Integers must sit in the range each row gives; a value outside it throws rather than being clamped.
- Class keys (owner scope, resources, owner resolver) must name a class of the right kind; anything else throws, naming the key.
The config file
use RoundlyConsulting\Imports\Http\Resources\ImportActorResource;
use RoundlyConsulting\Imports\Http\Resources\ImportOwnerResource;
use RoundlyConsulting\Imports\Http\Resources\ImportRecordResource;
use RoundlyConsulting\Imports\Models\ImportBatch;
use RoundlyConsulting\Imports\Models\ImportedRecord;
use RoundlyConsulting\Imports\Models\ImportRowResult;
use RoundlyConsulting\Imports\Models\ImportSession;
use RoundlyConsulting\Imports\Scoping\ExplicitOwnerScope;
return [
'importers' => [],
'key_type' => env('IMPORTS_KEY_TYPE', 'bigint'),
'record_key_type' => env('IMPORTS_RECORD_KEY_TYPE'),
'primary_key_type' => env('IMPORTS_PRIMARY_KEY_TYPE', 'bigint'),
'models' => [
'session' => ImportSession::class,
'batch' => ImportBatch::class,
'row_result' => ImportRowResult::class,
'record' => ImportedRecord::class,
],
'tables' => [
'sessions' => 'import_sessions',
'batches' => 'import_batches',
'row_results' => 'import_row_results',
'records' => 'imported_records',
],
'owner_scope' => ExplicitOwnerScope::class,
'storage' => [
'disk' => env('IMPORTS_DISK', 'local'), // never a public disk
'path' => 'imports',
'retention' => env('IMPORTS_RETENTION', 'P30D'),
'tmp_path' => env('IMPORTS_TMP_PATH'), // blank = sys_get_temp_dir()/roundly-imports
'tmp_ttl' => 'PT6H',
],
'history' => [
'retention' => env('IMPORTS_HISTORY_RETENTION'), // blank = keep forever
],
'limits' => [
'file_size' => env('IMPORTS_MAX_FILE_SIZE', 10240), // KB
'rows' => env('IMPORTS_MAX_ROWS', 10000), // hard ceiling 100,000
'columns' => env('IMPORTS_MAX_COLUMNS', 100), // hard ceiling 16,384
'cell_length' => 32767,
],
'csv' => [
'delimiters' => [',', ';'],
'sniff_bytes' => 65536,
'unescape_formula_prefix' => true,
],
'xlsx' => [
'max_entries' => 1000,
'max_part_size' => 268435456,
'max_inflated' => 536870912,
'max_blank_rows' => 10000,
'shared_strings_memory' => 2097152,
'allow_macro_enabled' => false,
],
'headers' => [
'locales' => ['en', 'sk'],
'fuzzy' => true,
'fuzzy_threshold' => 0.85,
],
'values' => [
'date_formats' => ['Y-m-d', 'd.m.Y', 'j.n.Y', 'j. n. Y'],
'datetime_formats' => ['Y-m-d H:i:s', 'Y-m-d H:i', 'd.m.Y H:i', 'j.n.Y H:i'],
'timezone' => null, // blank = the application timezone
],
'preview' => [
'sync_threshold' => env('IMPORTS_SYNC_THRESHOLD', 500),
'window' => 500,
'progress_every' => 250,
],
'commit' => [
'chunk_size' => 250,
'max_chunks' => 100,
'tries' => 3,
'backoff' => [10, 60, 300],
'timeout' => 600,
],
'undo' => [
'window' => env('IMPORTS_UNDO_WINDOW', 'P7D'),
],
'sessions' => [
'expire_after' => 'P2D',
'stale_after' => 'PT30M',
'abandon_after' => 'PT6H',
],
'queue' => [
'enabled' => env('IMPORTS_QUEUE_ENABLED', true),
'connection' => env('IMPORTS_QUEUE_CONNECTION'),
'queue' => env('IMPORTS_QUEUE_NAME'),
],
'files' => [
'max_files' => 500,
'max_uncompressed' => 1073741824,
'max_ratio' => 100,
'ratio_min_size' => 1048576,
'max_file_size' => 51200, // KB
'max_archive_size' => 1048576, // KB
'ignore' => ['__MACOSX/*', '*.DS_Store', 'Thumbs.db', 'desktop.ini'],
],
'templates' => [
'default_format' => 'xlsx',
'example_row' => true,
'hint_row' => ['csv' => true, 'xlsx' => false],
'data_validation' => true,
'csv_delimiter' => ['default' => ',', 'sk' => ';'],
'csv_bom' => true,
],
'mappings' => [
'enabled' => true,
'max_per_importer' => 20,
'max_bytes' => 60000,
'lock_store' => env('IMPORTS_LOCK_STORE'),
],
'resources' => [
'owner' => ImportOwnerResource::class,
'actor' => ImportActorResource::class,
'record' => ImportRecordResource::class,
],
'routes' => [
'enabled' => env('IMPORTS_ROUTES') ?: 'off',
'prefix' => 'api/imports',
'middleware' => ['api', 'auth'],
'upload_limit' => env('IMPORTS_UPLOAD_LIMIT', 10),
'owner_resolver' => null,
],
];Importers, key types, models and tables
| Key | Env | Default | Purpose |
|---|---|---|---|
importers | — | [] | Classes extending Importer or FileImporter. More at runtime with Imports::register(). |
key_type | IMPORTS_KEY_TYPE | bigint | bigint, uuid or ulid — the owner and actor morphs. Must equal options.key_type: saved mappings are owner-scoped options. |
record_key_type | IMPORTS_RECORD_KEY_TYPE | blank = key_type | The imported-record morph — every importer model’s key type. |
primary_key_type | IMPORTS_PRIMARY_KEY_TYPE | bigint | The package tables’ own ids. Read by the migrations — set it before migrating. |
models.session / batch / row_result / record | — | ImportSession / ImportBatch / ImportRowResult / ImportedRecord | Swap a model by extending the packaged class and naming the subclass here. |
tables.sessions / batches / row_results / records | — | import_sessions / import_batches / import_row_results / imported_records | ^[a-z_][a-z0-9_]{0,29}$ — at most 30 characters, so derived index names stay under MySQL’s 64. |
owner_scope | — | ExplicitOwnerScope::class | A Contracts\OwnerScope class — keeps every lookup and write inside the owner (see Owners and permissions). |
Storage and retention
| Key | Env | Default | Purpose |
|---|---|---|---|
storage.disk | IMPORTS_DISK | local | Where uploads are stored. Never a public disk — imports:doctor reports one as an error. |
storage.path | — | imports | Files go under <path>/<owner digest>/<session id>/. |
storage.retention | IMPORTS_RETENTION | P30D | Source files, working copies and row results are deleted by imports:prune after this. |
storage.tmp_path | IMPORTS_TMP_PATH | blank = sys_get_temp_dir()/roundly-imports | Temp files — a non-local disk is copied here when a file needs random access. |
storage.tmp_ttl | — | PT6H | Orphaned temp directories older than this are removed by imports:sweep. |
history.retention | IMPORTS_HISTORY_RETENTION | blank = keep forever | Sessions, batches and ledger rows past it and past their undo window are deleted by imports:prune. |
Limits
| Key | Env | Default | Purpose |
|---|---|---|---|
limits.file_size | IMPORTS_MAX_FILE_SIZE | 10240 | KB, 1 … 2,097,152 — checked before the file is read. |
limits.rows | IMPORTS_MAX_ROWS | 10000 | Data rows, 1 … 100,000. |
limits.columns | IMPORTS_MAX_COLUMNS | 100 | 1 … 16,384; non-empty cells only, so formatted empty columns do not count. |
limits.cell_length | — | 32767 | Characters per cell, 1 … 1,048,576. An importer overrides it with limits()->withCellLength(). |
Reading CSV and XLSX
| Key | Env | Default | Purpose |
|---|---|---|---|
csv.delimiters | — | [',', ';'] | One-character sniffing candidates. |
csv.sniff_bytes | — | 65536 | 1,024 … 1,048,576 bytes read to sniff the delimiter. |
csv.unescape_formula_prefix | — | true | Strip the ' the package’s own CSV writer puts before formula-like cells. |
xlsx.max_entries | — | 1000 | 1 … 100,000 ZIP entries in a workbook. |
xlsx.max_part_size | — | 268435456 | ≥ 1,024 bytes per part, counted while inflating (256 MB). |
xlsx.max_inflated | — | 536870912 | ≥ 1,024 bytes across one read (512 MB). |
xlsx.max_blank_rows | — | 10000 | ≥ 1 — the longest run of empty rows allowed between two rows with data. Data below a longer gap rejects the file (data_after_blank_rows); a formatted blank tail with nothing below is ignored. |
xlsx.shared_strings_memory | — | 2097152 | ≥ 1,024 bytes of shared strings kept in memory, then spooled to disk (2 MB). |
xlsx.allow_macro_enabled | — | false | Accept .xlsm workbooks — macros never run. |
Headers and values
| Key | Env | Default | Purpose |
|---|---|---|---|
headers.locales | — | ['en', 'sk'] | Locales whose labels match headers, plus the current and fallback locale. |
headers.fuzzy | — | true | Suggest close spellings — flagged, never applied without review. |
headers.fuzzy_threshold | — | 0.85 | 0.5 … 1.0. |
values.date_formats | — | ['Y-m-d', 'd.m.Y', 'j.n.Y', 'j. n. Y'] | PHP createFromFormat formats tried for date text; a field adds its own with formats(). |
values.datetime_formats | — | ['Y-m-d H:i:s', 'Y-m-d H:i', 'd.m.Y H:i', 'j.n.Y H:i'] | The same for date-times. |
values.timezone | — | blank = app.timezone | A timezone identifier — the clock the file’s dates and times are read in. DateTime values are then converted to app.timezone before an importer sees them; Date values are not. |
Preview, commit, undo and sessions
| Key | Env | Default | Purpose |
|---|---|---|---|
preview.sync_threshold | IMPORTS_SYNC_THRESHOLD | 500 | 0 … 100,000 — up to this many data rows validate in the request, more on the queue. Also the inline rollback size. |
preview.window | — | 500 | 1 … 10,000 rows per validation window. |
preview.progress_every | — | 250 | 1 … 100,000 rows between ImportPreviewProgressed events. |
commit.chunk_size | — | 250 | 1 … 10,000 rows per chunk. |
commit.max_chunks | — | 100 | 1 … 1,000 — the chunk size grows to fit. |
commit.tries | — | 3 | 1 … 100 attempts per chunk job. |
commit.backoff | — | [10, 60, 300] | Non-negative seconds between attempts. |
commit.timeout | — | 600 | 1 … 86,400 seconds — keep the queue’s retry_after above it (the doctor checks). |
undo.window | IMPORTS_UNDO_WINDOW | P7D | How long a finished import can be undone. |
sessions.expire_after | — | P2D | Uncommitted sessions expire (imports:sweep). |
sessions.stale_after | — | PT30M | No progress for this long and no live job → failed. |
sessions.abandon_after | — | PT6H | Silent for this long → failed regardless. |
Queue
| Key | Env | Default | Purpose |
|---|---|---|---|
queue.enabled | IMPORTS_QUEUE_ENABLED | true | Off: previews validate in the request and batches run on the sync connection. |
queue.connection | IMPORTS_QUEUE_CONNECTION | blank = default | The connection of previews, commits and rollbacks. |
queue.queue | IMPORTS_QUEUE_NAME | blank = default | The queue name. |
File batches
| Key | Env | Default | Purpose |
|---|---|---|---|
files.max_files | — | 500 | 1 … 100,000 files or ZIP entries per batch. |
files.max_uncompressed | — | 1073741824 | ≥ 1,024 bytes — the declared total of an archive (1 GB). |
files.max_ratio | — | 100 | 1 … 100,000 — the compression ratio allowed. |
files.ratio_min_size | — | 1048576 | ≥ 0 bytes — the ratio applies above this size (1 MB). |
files.max_file_size | — | 51200 | KB — the per-file default when FileRules sets none. |
files.max_archive_size | — | 1048576 | KB per archive. |
files.ignore | — | ['__MACOSX/*', '*.DS_Store', 'Thumbs.db', 'desktop.ini'] | Entries skipped. |
Templates
| Key | Env | Default | Purpose |
|---|---|---|---|
templates.default_format | — | xlsx | xlsx or csv. |
templates.example_row | — | true | Write an example row. |
templates.hint_row.csv / .xlsx | — | true / false | Write a hint row (required, allowed values, formats). |
templates.data_validation | — | true | XLSX dropdowns for choice and yes/no columns. |
templates.csv_delimiter | — | ['default' => ',', 'sk' => ';'] | Per locale; default is required. |
templates.csv_bom | — | true | Start CSV templates with a UTF-8 BOM. |
Saved mappings
| Key | Env | Default | Purpose |
|---|---|---|---|
mappings.enabled | — | true | Off: no options table needed, and saving throws InvalidSavedMappingException::disabled(). |
mappings.max_per_importer | — | 20 | 1 … 1,000 per owner and importer. |
mappings.max_bytes | — | 60000 | 1,024 … 16,000,000 — the options value column is text (64 KB on MySQL). |
mappings.lock_store | IMPORTS_LOCK_STORE | blank = default cache store | Serialises saves — use a shared store (redis, database). |
HTTP
| Key | Env | Default | Purpose |
|---|---|---|---|
resources.owner / actor / record | — | ImportOwnerResource / ImportActorResource / ImportRecordResource | An instantiable JsonResource subclass for each reference (see JSON API). |
routes.enabled | IMPORTS_ROUTES | off | The JSON API — on/off (any boolean). |
routes.prefix | — | api/imports | The URL prefix. |
routes.middleware | — | ['api', 'auth'] | Route middleware. |
routes.upload_limit | IMPORTS_UPLOAD_LIMIT | 10 | 0 … 10,000 uploads per minute per authenticated user (per IP for guests), both upload endpoints; 0 = no limit. |
routes.owner_resolver | — | null = the authenticated user | A Contracts\ResolvesImportOwner class. |
Environment
IMPORTS_KEY_TYPE=bigint
IMPORTS_RECORD_KEY_TYPE=
IMPORTS_PRIMARY_KEY_TYPE=bigint
IMPORTS_DISK=local
IMPORTS_RETENTION=P30D
IMPORTS_TMP_PATH=
IMPORTS_HISTORY_RETENTION=
IMPORTS_MAX_FILE_SIZE=10240
IMPORTS_MAX_ROWS=10000
IMPORTS_MAX_COLUMNS=100
IMPORTS_SYNC_THRESHOLD=500
IMPORTS_UNDO_WINDOW=P7D
IMPORTS_QUEUE_ENABLED=true
IMPORTS_QUEUE_CONNECTION=
IMPORTS_QUEUE_NAME=
IMPORTS_LOCK_STORE=redis
IMPORTS_ROUTES=off
IMPORTS_UPLOAD_LIMIT=10Show 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.