---
title: "Installation — Imports for Laravel | Roundly"
description: "Install via Composer, publish the imports and options migrations, create job_batches, schedule sweep and prune, settle key types, write an importer."
url: https://roundly-consulting.com/open-source/docs/imports-for-laravel/installation
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Imports for Laravel](https://roundly-consulting.com/open-source/docs/imports-for-laravel.md)

# Installation

Require the package, publish its migrations and options-for-laravel’s (saved column mappings live there), add Laravel’s job\_batches table if you don’t have it, and migrate. The service provider and the Imports facade alias are auto-discovered, together with the providers of the Roundly packages it builds on — crypto, enums, options and package-toolkit:

```bash
composer require roundly-consulting/imports-for-laravel
php artisan vendor:publish --tag="options-migrations"   # saved column mappings live in options-for-laravel
php artisan vendor:publish --tag="imports-migrations"
php artisan make:queue-batches-table   # Laravel's job_batches table, if you do not have it yet
php artisan migrate
```

The migrations are publish-only and forward-only. Four tables are created: import\_sessions, import\_batches, import\_row\_results and imported\_records. Every upload reads the owner’s saved mappings, so the options table is required while imports.mappings.enabled is on (the default) — without it an upload fails with InvalidImportsConfigurationException naming the fix, never a raw SQL error.

Or let the install command do the publishing:

```bash
# Publishes the config, the imports migrations and options-for-laravel's migration
# (skipped when the options table exists), then prints the remaining steps.
# Refuses (exit 1) when an imports table already exists.
php artisan imports:install
```

## 1\. Schedule sweep and prune

Nothing is scheduled automatically. imports:sweep expires forgotten sessions and releases stuck previews, commits and rollbacks; imports:prune deletes files and row results past retention:

```php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('imports:sweep')->everyFiveMinutes()->withoutOverlapping(10)->onOneServer();
Schedule::command('imports:prune')->daily()->onOneServer();
```

Run a queue worker too: previews above imports.preview.sync\_threshold (500 rows) and every commit run on the queue, unless imports.queue.enabled is off. Keep the queue connection’s retry\_after above imports.commit.timeout (600 seconds).

## 2\. Settle the key types before migrating

With bigint ids everywhere the defaults fit. With UUID or ULID keys, set them before php artisan migrate:

```bash
# Before php artisan migrate — bigint, uuid or ulid
IMPORTS_KEY_TYPE=uuid            # owner + actor morphs — must equal options.key_type
IMPORTS_RECORD_KEY_TYPE=         # the imported records' key type; blank = IMPORTS_KEY_TYPE
IMPORTS_PRIMARY_KEY_TYPE=bigint  # the package tables' own ids
```

A model whose key does not fit is refused before any write with InvalidImportsConfigurationException naming the config key and the model.

## 3\. Pick a private disk

Uploads are stored on imports.storage.disk (default local) under imports/<owner digest>/<session id>/. Any Laravel disk works — a non-local one is copied to a local temp file when a file needs random access — but never a public disk: uploads hold personal data, and imports:doctor reports a public disk as an error.

## 4\. Write an importer and import a file

Describe one record type and list it in config('imports.importers'):

```php
use App\Models\Asset;
use App\Models\Vendor;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Imports\Definition\{Field, NaturalKey, Ownership};
use RoundlyConsulting\Imports\Enums\ImportOperation;
use RoundlyConsulting\Imports\Importer;
use RoundlyConsulting\Imports\Rows\{ImportContext, ImportRow};

final class AssetsImporter extends Importer   // key `assets`, label `Assets`
{
    public function model(): string { return Asset::class; }
    public function ownership(): Ownership { return Ownership::column('team_id'); }
    public function naturalKey(): NaturalKey { return NaturalKey::by('asset_tag')->then('serial_number'); }

    public function fields(): array
    {
        return [
            Field::text('asset_tag')->aliases('tag', 'inventory no'),
            Field::text('serial_number'),
            Field::text('name')->required()->max(120),
            Field::date('purchased_on'),
            Field::reference('vendor')->to(Vendor::class)->by('vat_id', 'name')->storeAs('vendor_id'),
        ];
    }

    public function authorize(?Authenticatable $user, Model $owner, ImportOperation $operation): bool
    {
        return $user?->can('manage-assets', $owner) ?? false;
    }

    public function create(ImportRow $row, ImportContext $context): Model
    {
        return Asset::create([...$row->attributes(), ...$context->ownerAttributes()]);
    }

    public function update(Model $asset, ImportRow $row, ImportContext $context): Model
    {
        return tap($asset)->update($row->changes());   // blank cells keep the stored value
    }
}
```

Then upload, preview, commit — and undo if needed:

```php
use RoundlyConsulting\Imports\Enums\DuplicateMode;
use RoundlyConsulting\Imports\Facades\Imports;

$import = Imports::for($team)->by($user)
    ->upload('assets', $request->file('file'))   // CSV or XLSX; columns map from the headers
    ->mode(DuplicateMode::Update)
    ->preview();   // ≤ 500 rows: checked now; larger files validate on the queue — poll isPreviewed()

$import->counts();                               // valid 47 · invalid 2 · duplicates 1
$batch = $import->commit();                      // a background job batch
$batch->rollback();                              // later: removes the rows it created, unless edited since
```

## Publish tags

| Tag | Publishes |
| --- | --- |
| `imports-migrations` | The four migrations, timestamped on publish. Required: they are publish-only and forward-only. |
| `options-migrations` | options-for-laravel’s table — saved column mappings live there. Required while imports.mappings.enabled is on. |
| `imports-config` | config/imports.php |
| `imports-translations` | lang/vendor/imports/{en,sk}/ — booleans, enums, exceptions, preview, rejections, templates, validation |
| `imports-routes` | routes/imports.php — once published it loads instead of the package’s file, still only when imports.routes.enabled is on. |

```bash
php artisan vendor:publish --tag="imports-config"         # config/imports.php
php artisan vendor:publish --tag="imports-translations"   # lang/vendor/imports/{en,sk}/
php artisan vendor:publish --tag="imports-routes"         # routes/imports.php — loaded instead of the package's
```

## Tables

| Table | Holds |
| --- | --- |
| `import_sessions` | One row per upload (soft-deleted): importer, kind, owner and actor morphs, status, the source (disk, path, name, size, SHA-256, format, encoding, delimiter, sheet, header row, headers), mapping, mode, locale, timezone, counts, progress, preview\_version, chunk offsets, the XLSX working copy and a unique lock\_key — never row values. |
| `import_batches` | One per committed session: committer and undoer morphs, the Laravel job\_batches ids, status, chunk counters, summary, rollback status and counts, failure, started\_at, finished\_at, undo\_until. |
| `import_row_results` | Exceptions only — invalid, duplicate, matched and failed rows (and every entry of a file batch): status, outcome, cell errors, the natural-key match. Never row values. |
| `imported_records` | The ledger: one row per record a commit created, linked or updated — the polymorphic “imported from” pivot, with a checksum for undo, the owner digest and, for files, the content hash. Unique (batch\_id, row\_number). |

Table names are configurable (imports.tables.\*). Every datetime is stored as a UTC string and read back as a UTC CarbonImmutable. Saved mappings are not in these tables: they live in options-for-laravel’s options table, one row per owner.

## Translations

Everything the package says to a user — rejections, validation messages, template hints, enum labels, exceptions — ships in English and Slovak (imports::…). Add a locale by publishing the translations and copying a folder.

## Check the setup

```bash
php artisan imports:doctor
php artisan about --only=imports
```

The doctor prints one finding per check (see Artisan commands) and exits non-zero when any is an error. The about section shows the importer count, the queue (connection and queue only as SET or DEFAULT), the disk, the limits, routes, the three key types and the owner scope.

## Upgrading from 1.0.0

No migration and no config change. Three things may need a hand:

- Breaking: the stored-count reader on the ImportSession and ImportBatch models is renamed from count(string $key) to countOf(string $key). The static ImportSession::count() and ImportBatch::count() are Eloquent’s row count again.
- DateTime field values now reach your importer converted to app.timezone; before, they were the wall clock in imports.values.timezone. With a blank values.timezone (the default) nothing changes, and Date fields never change.
- options() with numeric keys keeps them as the stored values: \[1 => 'Male', 2 => 'Female'\] now stores 1 and 2. Only a true list (keys 0, 1, 2, …) is read as values without labels.

```php
use RoundlyConsulting\Imports\Models\ImportSession;

// Before: a stored count on an ImportSession or ImportBatch model
$model->count('valid');

// Now
$model->countOf('valid');          // one stored count, 0 when absent
ImportSession::count();            // Eloquent's row count again (the same on ImportBatch)
```

```php
// config/app.php: 'timezone' => 'UTC' · config/imports.php: 'values' => ['timezone' => 'Europe/Bratislava']
// A DateTime cell 2026-01-15 09:30

$row->get('accepted_at');
// before: 2026-01-15 09:30 in Europe/Bratislava (the wall clock)
// now:    2026-01-15 08:30 UTC — the same instant, in app.timezone, which is what Eloquent stores
```

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/imports-for-laravel.md)
