NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
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

KeyEnvDefaultPurpose
importers—[]Classes extending Importer or FileImporter. More at runtime with Imports::register().
key_typeIMPORTS_KEY_TYPEbigintbigint, uuid or ulid — the owner and actor morphs. Must equal options.key_type: saved mappings are owner-scoped options.
record_key_typeIMPORTS_RECORD_KEY_TYPEblank = key_typeThe imported-record morph — every importer model’s key type.
primary_key_typeIMPORTS_PRIMARY_KEY_TYPEbigintThe package tables’ own ids. Read by the migrations — set it before migrating.
models.session / batch / row_result / record—ImportSession / ImportBatch / ImportRowResult / ImportedRecordSwap 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::classA Contracts\OwnerScope class — keeps every lookup and write inside the owner (see Owners and permissions).

Storage and retention

KeyEnvDefaultPurpose
storage.diskIMPORTS_DISKlocalWhere uploads are stored. Never a public disk — imports:doctor reports one as an error.
storage.path—importsFiles go under <path>/<owner digest>/<session id>/.
storage.retentionIMPORTS_RETENTIONP30DSource files, working copies and row results are deleted by imports:prune after this.
storage.tmp_pathIMPORTS_TMP_PATHblank = sys_get_temp_dir()/roundly-importsTemp files — a non-local disk is copied here when a file needs random access.
storage.tmp_ttl—PT6HOrphaned temp directories older than this are removed by imports:sweep.
history.retentionIMPORTS_HISTORY_RETENTIONblank = keep foreverSessions, batches and ledger rows past it and past their undo window are deleted by imports:prune.

Limits

KeyEnvDefaultPurpose
limits.file_sizeIMPORTS_MAX_FILE_SIZE10240KB, 1 … 2,097,152 — checked before the file is read.
limits.rowsIMPORTS_MAX_ROWS10000Data rows, 1 … 100,000.
limits.columnsIMPORTS_MAX_COLUMNS1001 … 16,384; non-empty cells only, so formatted empty columns do not count.
limits.cell_length—32767Characters per cell, 1 … 1,048,576. An importer overrides it with limits()->withCellLength().

Reading CSV and XLSX

KeyEnvDefaultPurpose
csv.delimiters—[',', ';']One-character sniffing candidates.
csv.sniff_bytes—655361,024 … 1,048,576 bytes read to sniff the delimiter.
csv.unescape_formula_prefix—trueStrip the ' the package’s own CSV writer puts before formula-like cells.
xlsx.max_entries—10001 … 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—falseAccept .xlsm workbooks — macros never run.

Headers and values

KeyEnvDefaultPurpose
headers.locales—['en', 'sk']Locales whose labels match headers, plus the current and fallback locale.
headers.fuzzy—trueSuggest close spellings — flagged, never applied without review.
headers.fuzzy_threshold—0.850.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.timezoneA 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

KeyEnvDefaultPurpose
preview.sync_thresholdIMPORTS_SYNC_THRESHOLD5000 … 100,000 — up to this many data rows validate in the request, more on the queue. Also the inline rollback size.
preview.window—5001 … 10,000 rows per validation window.
preview.progress_every—2501 … 100,000 rows between ImportPreviewProgressed events.
commit.chunk_size—2501 … 10,000 rows per chunk.
commit.max_chunks—1001 … 1,000 — the chunk size grows to fit.
commit.tries—31 … 100 attempts per chunk job.
commit.backoff—[10, 60, 300]Non-negative seconds between attempts.
commit.timeout—6001 … 86,400 seconds — keep the queue’s retry_after above it (the doctor checks).
undo.windowIMPORTS_UNDO_WINDOWP7DHow long a finished import can be undone.
sessions.expire_after—P2DUncommitted sessions expire (imports:sweep).
sessions.stale_after—PT30MNo progress for this long and no live job → failed.
sessions.abandon_after—PT6HSilent for this long → failed regardless.

Queue

KeyEnvDefaultPurpose
queue.enabledIMPORTS_QUEUE_ENABLEDtrueOff: previews validate in the request and batches run on the sync connection.
queue.connectionIMPORTS_QUEUE_CONNECTIONblank = defaultThe connection of previews, commits and rollbacks.
queue.queueIMPORTS_QUEUE_NAMEblank = defaultThe queue name.

File batches

KeyEnvDefaultPurpose
files.max_files—5001 … 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—1001 … 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—51200KB — the per-file default when FileRules sets none.
files.max_archive_size—1048576KB per archive.
files.ignore—['__MACOSX/*', '*.DS_Store', 'Thumbs.db', 'desktop.ini']Entries skipped.

Templates

KeyEnvDefaultPurpose
templates.default_format—xlsxxlsx or csv.
templates.example_row—trueWrite an example row.
templates.hint_row.csv / .xlsx—true / falseWrite a hint row (required, allowed values, formats).
templates.data_validation—trueXLSX dropdowns for choice and yes/no columns.
templates.csv_delimiter—['default' => ',', 'sk' => ';']Per locale; default is required.
templates.csv_bom—trueStart CSV templates with a UTF-8 BOM.

Saved mappings

KeyEnvDefaultPurpose
mappings.enabled—trueOff: no options table needed, and saving throws InvalidSavedMappingException::disabled().
mappings.max_per_importer—201 … 1,000 per owner and importer.
mappings.max_bytes—600001,024 … 16,000,000 — the options value column is text (64 KB on MySQL).
mappings.lock_storeIMPORTS_LOCK_STOREblank = default cache storeSerialises saves — use a shared store (redis, database).

HTTP

KeyEnvDefaultPurpose
resources.owner / actor / record—ImportOwnerResource / ImportActorResource / ImportRecordResourceAn instantiable JsonResource subclass for each reference (see JSON API).
routes.enabledIMPORTS_ROUTESoffThe JSON API — on/off (any boolean).
routes.prefix—api/importsThe URL prefix.
routes.middleware—['api', 'auth']Route middleware.
routes.upload_limitIMPORTS_UPLOAD_LIMIT100 … 10,000 uploads per minute per authenticated user (per IP for guests), both upload endpoints; 0 = no limit.
routes.owner_resolver—null = the authenticated userA 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=10

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.