Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
kinetix-import-export
kinetix-import-export
happones/kinetix
claude-code
imports
exports
csv
column-mapping
queues
vue
validation

Handles CSV/Excel import through a three-step wizard (file → mapping → review) and queued export with a download notification in Kinetix. Activates when building Importer/Exporter classes, ImportColumn/ExportColumn definitions, the import wizard UI, tuning the import preview/upload/layout limits, or the queued import/export jobs.

Install
php artisan boost:add-skill happones/kinetix

Save this content to: .claude/skills/kinetix-import-export/SKILL.md

# Kinetix Import / Export Development

## When to Apply

Activate this skill when:
- Building an `Importer` (CSV/Excel ingestion) or `Exporter` (CSV/Excel generation).
- Defining `ImportColumn` / `ExportColumn` mappings, validation rules, alias guesses, or value casting/formatting.
- Wiring the import wizard UI (`KinetixImporter.vue`, `components/Importer/*`, `useKinetixImporter`) or the import endpoints (`imports/upload|preview|start`).
- Tuning what the import dialog reads or shows: preview rows/columns, upload ceiling, dialog surface (modal / full-screen / sheet).
- Importing a large file (tens of thousands of rows and up) and reasoning about memory or read cost.
- Working with the queued `ImportProcessor` / `ExportProcessor` jobs or the export download route.
- Customizing the dispatched job (chunking, queue, per-row `importRow()`/`resolveRecord()`, scoped export `query()`).

## Documentation

Full reference: [Kinetix Import / Export Documentation](https://happones.github.io/kinetix/import-export).

## Usage Guide

### 1. Importer + smart preview

```php
use Happones\Kinetix\Imports\Importer;
use Happones\Kinetix\Imports\ImportColumn;

class ContactImporter extends Importer
{
    protected static ?string $model = \App\Models\Contact::class;

    public static function getColumns(): array
    {
        return [
            ImportColumn::make('name')->guess(['nombre'])->requiredMapping(),
            ImportColumn::make('email')->guess(['e-mail'])->rules(['email'])
                ->castStateUsing(fn ($v) => strtolower(trim((string) $v))),
            ImportColumn::make('phone')->guess(['celular', 'mobile']),
        ];
    }

public function resolveRecord(array $data): ?\Illuminate\Database\Eloquent\Model
    {
        return \App\Models\Contact::firstOrNew(['email' => $data['email'] ?? null]);
    }
}
```

```php
// Controller → page
return inertia('Contacts/Import', ['importer' => ContactImporter::token()]);
```

```vue
<!-- Standalone on a page (the page scrolls). -->
<KinetixImporter :importer="importer" />
```

Or, with zero page wiring, from a table action — mount `<KinetixImportModal />`
once in your layout and the dialog picks its own surface:

```php
use Happones\Kinetix\Actions\ImportAction;

$table->headerActions([ImportAction::make()->importer(ContactImporter::class)]);
```

### 1b. Tuning the dialog per importer

```php
class WideProductImporter extends Importer
{
    protected ?int $previewRows = 5;              // sample rows — AND the read ceiling
    protected ?int $previewColumns = 0;           // 0 = no column cap in the preview
    protected ?string $layout = 'fullscreen';     // 'auto'|'modal'|'fullscreen'|'sheet'
    protected ?int $fullscreenThreshold = 8;      // when 'auto' goes full screen
    protected ?int $maxUploadSize = 512000;       // KB
}

class SensitiveImporter extends Importer
{
    protected ?bool $preview = false;             // map the columns, never show the cells
}
```

Everything falls back to `config('kinetix.imports.*')`
(`max_upload_size`, `preview`, `preview_rows`, `preview_columns`, `layout`,
`fullscreen_threshold`, `spreadsheet_chunk_size`, `transaction_per_row`).

### 1c. Validating rows like a FormRequest

```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;

class EmployeeImporter extends Importer
{
    protected static ?string $model = \App\Models\Employee::class;

    // Optional: also validate with the screen's FormRequest / Form / Resource.
    protected ?string $validateUsing = \App\Http\Requests\StoreEmployeeRequest::class;

public static function getColumns(): array
    {
        return [
            ImportColumn::make('name')->rules('required|string|max:120'), // pipes OK
            ImportColumn::make('phone')->labelKey('employee.phone')->aliases(['Celular']),
            ImportColumn::make('pin')->sensitive(), // blanked in the failed-rows CSV
        ];
    }

    protected function prepareRow(array $row): array { /* normalize BEFORE validation */ return $row; }

    protected function rules(array $row, ?Model $record): array
    {
        return ['email' => ['nullable', 'email', Rule::unique('employees')
            ->where('team_id', $this->context['team_id'])->ignore($record)]];
    }

protected function after(array $row, ?Model $record): array
    {
        return [fn (Validator $v) => /* cross-field checks */ null];
    }

    public function importRow(array $data): void
    {
        $record = $this->record($data) ?? $this->reject('No employee has that number.');
        // …
    }
}
```

### 1d. Import modes — what happens to existing records

```php
use Happones\Kinetix\Imports\ImportMode;

class CustomerImporter extends Importer
{
    protected static ?string $model = \App\Models\Customer::class; // SoftDeletes or an `active` column

// Opt-in: the review step offers these. Create = only add (matches fail),
    // Upsert = update matches + add, Update = only update (no match fails),
    // Replace = upsert + reactivate matches, then deactivate every record the file left out.
    protected array $importModes = [ImportMode::Upsert, ImportMode::Replace];

    protected ?ImportMode $defaultImportMode = ImportMode::Upsert;

    protected array $matchBy = ['email']; // or override resolveRecord()

    protected function query(): \Illuminate\Database\Eloquent\Builder
    {
        // REQUIRED when multi-tenant: Replace sweeps whatever this returns.
        return \App\Models\Customer::query()->where('team_id', $this->context['team_id']);
    }
}
```

Replace deactivates only at the end (never when no row imported), restores what comes back, and never erases. `authorizeMode()` can refuse a mode per user.

### 1e. Relationship columns — id OR name in the file

```php
ImportColumn::make('category')
    ->relationship('category', ['id', 'name'])            // BelongsTo: sets category_id; tries id, then name
    ->relationshipQuery(fn (Builder $q, array $context) => $q->where('team_id', $context['team_id'])), // REQUIRED for tenant data
ImportColumn::make('tags')->relationship('tags', 'name')->multiple(','), // BelongsToMany: synced after save
// ->createMissing(fn (string $v, array $context) => ['name' => $v, 'team_id' => $context['team_id']])  (off by default)
// ->caseSensitive()  (default insensitive)
```

Lookups are batched per chunk (distinct cells, one query per match column) and cached for the import; not found / ambiguous fails the row with the column label. Custom `importRow()`: `$data['category']` is the key (array for multiple) — call `$this->syncRelationships($record, $data)` after save.

### 2. Exporter + download notification

```php
use Happones\Kinetix\Exports\Exporter;
use Happones\Kinetix\Exports\ExportColumn;

class ContactExporter extends Exporter
{
    protected static ?string $model = \App\Models\Contact::class;

    public function format(): string { return 'xlsx'; } // 'csv' | 'xlsx' | 'xls' (Excel 97) | 'pdf'

public static function getColumns(): array
    {
        return [
            ExportColumn::make('name')->label('Full Name'),
            ExportColumn::make('email'),
        ];
    }
}

// Relation columns: dot notation of any depth — eager loaded per chunk (no N+1, safe with preventLazyLoading).
// ExportColumn::make('product.category.slug'); ExportColumn::make('tags.name')->separator(' | ')->distinct();
// Extra loads for formatStateUsing(): public function with(): array { return ['customer.address']; }
// Typed cells (xlsx/xls get real dates/numbers/text + number format; csv/pdf the formatted text):
// ExportColumn::make('birthday')->date('dd/mm/yyyy'); ->numeric('0.00'); ->text() (keeps leading zeros).
// Pages by key (chunkById) unless query() has an orderBy. Iterate with $exporter->eachChunk(fn ($records) => …).

// Dispatch — the recipient receives an "Export ready" notification with a Download action.
(new ContactExporter())->export($request->user());
```

---

## Best Practices

- **Smart mapping is server-driven**: `Importer::guessMapping($headers)` matches headers against each column's name/label/`aliases()` (normalized: case, spacing, punctuation and accents don't count; `labelKey()` labels match in every locale of `kinetix.locale.locales`) and is **collision-free** — one source header maps to at most one target. The Vue layer only disables already-claimed options; never re-implement matching client-side.
- **Security**: the importer/exporter class travels as an encrypted `token()`; stored files are referenced by encrypted tokens constrained to `kinetix-imports` / `kinetix-exports` (traversal-guarded). Keep that pattern for any new endpoint.
- **Scale is already handled — do not undo it (IMPORTANT)**: nothing in the import path may grow with the file. Previewing reads a SAMPLE (`FileReader::read($path, $options, $limit)` stops at the limit); the row count is counted, not parsed (CSV newline blocks; a spreadsheet's own `listWorksheetInfo()`); the queued job consumes `FileReader::stream()`, a generator, so it holds one `chunkSize()` chunk and never the file; failed rows are written to the downloadable CSV as they fail. **Never call `FileReader::read()` without a limit on a user-supplied file** and never collect rows into an array to process them — that is how a million-row import becomes a fatal memory error. Spreadsheets are read in windows of `spreadsheet_chunk_size` rows (`RowWindowFilter`), first worksheet only.
- **Exact matches skip a step**: `Importer::isExactMatch($headers, $mapping)` is true when every target column found a header AND every non-blank header was claimed — which a file filled in from the downloadable template always satisfies. The wizard then goes straight to review. It deliberately fails when a source column goes unclaimed, because that column's data would be silently dropped; the mapping step names those columns instead.
- **The dialog sizes itself, in one component**: under `layout: 'auto'` `KinetixImportModal` promotes the SAME `KinetixModal` to `fullscreen` past `fullscreenThreshold` columns, so the wizard is resized rather than remounted and the user's mapping survives. `'sheet'` is an explicit per-importer choice only — swapping shells mid-flow would unmount the wizard and lose the file.
- **The reading options are a `KinetixCollapsible`**: the shared shadcn/Reka disclosure (a real `<button>` with `aria-expanded`, animating its measured height), collapsed by default and keeping a `summary` line while folded — folded is not the same as unknown. Reuse it for any disclosure; never hand-roll a `v-show` toggle. Its field grid uses a **container query**, not `sm:`/`lg:`: the dialog's width does not follow the screen, so viewport breakpoints squeezed the fields into columns their labels could not fit.
- **`surface` decides who scrolls**: `inline` (the page), `modal` (the wizard's own capped scroller), `fullscreen` (its scroller fills the panel), `sheet` (none — `KinetixSheet` already scrolls). Pass it whenever you place `KinetixImporter` in a shell of your own.
- **Always queue heavy work**: both `ImportProcessor` and `ExportProcessor` are `ShouldQueue`. Process in chunked DB transactions (`chunkSize()`), then notify via the Kinetix `Notification` (`broadcast()` when Echo is configured, else `sendToDatabase()`).
- **Excel vs CSV vs PDF**: CSV is parsed/written natively (RFC-4180, empty escape); `.xls`/`.xlsx` go through `phpoffice/phpspreadsheet`; `pdf` renders a landscape-A4 HTML table via the **optional** `dompdf/dompdf` (install `composer require dompdf/dompdf`; throws a clear error if missing). All in `FileWriter` — add new formats there, not in the jobs.
- **Row pipeline (IMPORTANT)**: `processRow()` = `prepareRow()` → record resolved ONCE (`$this->record($data)`, shared by `rules()`/`after()`/`importRow()` — never call `resolveRecord()` again in a custom `importRow()`) → column rules + `rules()` + `$validateUsing` (merged per field) → `importRow()` → `castStateUsing()`. Each row runs in its own savepoint (`transaction_per_row`, default on). ANY throwable fails that row only — never let validation or rule building escape the per-row `try`. Fail a row on purpose with `$this->reject()` / `RowRejected`; a bare `return` counts as imported. Messages name fields by column label and every message is reported.
- **Customizing**: override `importRow()`/`resolveRecord()` for upsert logic, `query()` to scope an export, and `queue()`/`chunkSize()` for throughput — do not bypass the job.
- **Notification lifecycle is automatic**: wiring `ExportAction::exporter()` / `ImportAction::importer()` gives a queued toast immediately, a completion database notification (broadcast live when Echo is configured) and a `danger` notification if the job dies. Rows that fail to map/validate/save are **skipped, counted and reported** (status `warning`) — never fatal — and a failed import additionally attaches a **Download failed rows** CSV action (every failed row + reason, signed user-bound link). Customize every message by overriding `getStartedNotificationBody()`, `getCompletedNotificationTitle/Body(int $done, int $failed)` and `getFailedNotificationTitle/Body()` on the exporter/importer. Completion notifications require `KINETIX_DATABASE_NOTIFICATIONS=true` + the `notifications` table + `<KinetixNotifications />` mounted (or broadcasting) to be visible; they're team-stamped automatically when notifications are team-scoped.
- **Template download**: the modal offers "Download template" by default (CSV of the column labels — they auto-map on upload). Opt out with `protected bool $downloadableTemplate = false`; rename with `protected ?string $templateFileName` (default = studly class name `.csv`; `.csv` is appended when an override omits it — read the name via `getTemplateDownloadName()`). The template starts with a UTF-8 BOM (Excel/Windows) and `FileReader` skips a BOM on upload. Endpoint `GET {prefix}/imports/template?importer={token}`.
- **Multi-tenancy (IMPORTANT)**: the queued job has no request — never infer the tenant inside `importRow()` (e.g. `Team::first()` is a cross-tenant leak). Override `context(Request $request): array` to capture `['team_id' => $request->user()?->currentTeam?->getKey()]` at dispatch; it is serialized with the job and restored before any row — read it via `$this->context['team_id']` / `getContext()`. Exporters have the same hook: scope `query()` with `$this->context['team_id']` — an unscoped `query()` exports every tenant's rows.
- **i18n & docs**: any new option/label must be added to every shipped locale under `resources/lang/*/kinetix.php` and to `docs/import-export.md`.

Kinetix Import / Export Development

When to Apply

Activate this skill when:

  • Building an Importer (CSV/Excel ingestion) or Exporter (CSV/Excel generation).
  • Defining ImportColumn / ExportColumn mappings, validation rules, alias guesses, or value casting/formatting.
  • Wiring the import wizard UI (KinetixImporter.vue, components/Importer/*, useKinetixImporter) or the import endpoints (imports/upload|preview|start).
  • Tuning what the import dialog reads or shows: preview rows/columns, upload ceiling, dialog surface (modal / full-screen / sheet).
  • Importing a large file (tens of thousands of rows and up) and reasoning about memory or read cost.
  • Working with the queued ImportProcessor / ExportProcessor jobs or the export download route.
  • Customizing the dispatched job (chunking, queue, per-row importRow()/resolveRecord(), scoped export query()).

Documentation

Full reference: Kinetix Import / Export Documentation.

Usage Guide

1. Importer + smart preview

use Happones\Kinetix\Imports\Importer;
use Happones\Kinetix\Imports\ImportColumn;

class ContactImporter extends Importer
{
    protected static ?string $model = \App\Models\Contact::class;

    public static function getColumns(): array
    {
        return [
            ImportColumn::make('name')->guess(['nombre'])->requiredMapping(),
            ImportColumn::make('email')->guess(['e-mail'])->rules(['email'])
                ->castStateUsing(fn ($v) => strtolower(trim((string) $v))),
            ImportColumn::make('phone')->guess(['celular', 'mobile']),
        ];
    }

public function resolveRecord(array $data): ?\Illuminate\Database\Eloquent\Model
    {
        return \App\Models\Contact::firstOrNew(['email' => $data['email'] ?? null]);
    }
}
// Controller → page
return inertia('Contacts/Import', ['importer' => ContactImporter::token()]);
<!-- Standalone on a page (the page scrolls). -->
<KinetixImporter :importer="importer" />

Or, with zero page wiring, from a table action — mount <KinetixImportModal /> once in your layout and the dialog picks its own surface:

use Happones\Kinetix\Actions\ImportAction;

$table->headerActions([ImportAction::make()->importer(ContactImporter::class)]);

1b. Tuning the dialog per importer

class WideProductImporter extends Importer
{
    protected ?int $previewRows = 5;              // sample rows — AND the read ceiling
    protected ?int $previewColumns = 0;           // 0 = no column cap in the preview
    protected ?string $layout = 'fullscreen';     // 'auto'|'modal'|'fullscreen'|'sheet'
    protected ?int $fullscreenThreshold = 8;      // when 'auto' goes full screen
    protected ?int $maxUploadSize = 512000;       // KB
}

class SensitiveImporter extends Importer
{
    protected ?bool $preview = false;             // map the columns, never show the cells
}

Everything falls back to config('kinetix.imports.*') (max_upload_size, preview, preview_rows, preview_columns, layout, fullscreen_threshold, spreadsheet_chunk_size, transaction_per_row).

1c. Validating rows like a FormRequest

use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;

class EmployeeImporter extends Importer
{
    protected static ?string $model = \App\Models\Employee::class;

    // Optional: also validate with the screen's FormRequest / Form / Resource.
    protected ?string $validateUsing = \App\Http\Requests\StoreEmployeeRequest::class;

public static function getColumns(): array
    {
        return [
            ImportColumn::make('name')->rules('required|string|max:120'), // pipes OK
            ImportColumn::make('phone')->labelKey('employee.phone')->aliases(['Celular']),
            ImportColumn::make('pin')->sensitive(), // blanked in the failed-rows CSV
        ];
    }

    protected function prepareRow(array $row): array { /* normalize BEFORE validation */ return $row; }

    protected function rules(array $row, ?Model $record): array
    {
        return ['email' => ['nullable', 'email', Rule::unique('employees')
            ->where('team_id', $this->context['team_id'])->ignore($record)]];
    }

protected function after(array $row, ?Model $record): array
    {
        return [fn (Validator $v) => /* cross-field checks */ null];
    }

    public function importRow(array $data): void
    {
        $record = $this->record($data) ?? $this->reject('No employee has that number.');
        // …
    }
}

1d. Import modes — what happens to existing records

use Happones\Kinetix\Imports\ImportMode;

class CustomerImporter extends Importer
{
    protected static ?string $model = \App\Models\Customer::class; // SoftDeletes or an `active` column

// Opt-in: the review step offers these. Create = only add (matches fail),
    // Upsert = update matches + add, Update = only update (no match fails),
    // Replace = upsert + reactivate matches, then deactivate every record the file left out.
    protected array $importModes = [ImportMode::Upsert, ImportMode::Replace];

    protected ?ImportMode $defaultImportMode = ImportMode::Upsert;

    protected array $matchBy = ['email']; // or override resolveRecord()

    protected function query(): \Illuminate\Database\Eloquent\Builder
    {
        // REQUIRED when multi-tenant: Replace sweeps whatever this returns.
        return \App\Models\Customer::query()->where('team_id', $this->context['team_id']);
    }
}

Replace deactivates only at the end (never when no row imported), restores what comes back, and never erases. authorizeMode() can refuse a mode per user.

1e. Relationship columns — id OR name in the file

ImportColumn::make('category')
    ->relationship('category', ['id', 'name'])            // BelongsTo: sets category_id; tries id, then name
    ->relationshipQuery(fn (Builder $q, array $context) => $q->where('team_id', $context['team_id'])), // REQUIRED for tenant data
ImportColumn::make('tags')->relationship('tags', 'name')->multiple(','), // BelongsToMany: synced after save
// ->createMissing(fn (string $v, array $context) => ['name' => $v, 'team_id' => $context['team_id']])  (off by default)
// ->caseSensitive()  (default insensitive)

Lookups are batched per chunk (distinct cells, one query per match column) and cached for the import; not found / ambiguous fails the row with the column label. Custom importRow(): $data['category'] is the key (array for multiple) — call $this->syncRelationships($record, $data) after save.

2. Exporter + download notification

use Happones\Kinetix\Exports\Exporter;
use Happones\Kinetix\Exports\ExportColumn;

class ContactExporter extends Exporter
{
    protected static ?string $model = \App\Models\Contact::class;

    public function format(): string { return 'xlsx'; } // 'csv' | 'xlsx' | 'xls' (Excel 97) | 'pdf'

public static function getColumns(): array
    {
        return [
            ExportColumn::make('name')->label('Full Name'),
            ExportColumn::make('email'),
        ];
    }
}

// Relation columns: dot notation of any depth — eager loaded per chunk (no N+1, safe with preventLazyLoading).
// ExportColumn::make('product.category.slug'); ExportColumn::make('tags.name')->separator(' | ')->distinct();
// Extra loads for formatStateUsing(): public function with(): array { return ['customer.address']; }
// Typed cells (xlsx/xls get real dates/numbers/text + number format; csv/pdf the formatted text):
// ExportColumn::make('birthday')->date('dd/mm/yyyy'); ->numeric('0.00'); ->text() (keeps leading zeros).
// Pages by key (chunkById) unless query() has an orderBy. Iterate with $exporter->eachChunk(fn ($records) => …).

// Dispatch — the recipient receives an "Export ready" notification with a Download action.
(new ContactExporter())->export($request->user());

Best Practices

  • Smart mapping is server-driven: Importer::guessMapping($headers) matches headers against each column's name/label/aliases() (normalized: case, spacing, punctuation and accents don't count; labelKey() labels match in every locale of kinetix.locale.locales) and is collision-free — one source header maps to at most one target. The Vue layer only disables already-claimed options; never re-implement matching client-side.
  • Security: the importer/exporter class travels as an encrypted token(); stored files are referenced by encrypted tokens constrained to kinetix-imports / kinetix-exports (traversal-guarded). Keep that pattern for any new endpoint.
  • Scale is already handled — do not undo it (IMPORTANT): nothing in the import path may grow with the file. Previewing reads a SAMPLE (FileReader::read($path, $options, $limit) stops at the limit); the row count is counted, not parsed (CSV newline blocks; a spreadsheet's own listWorksheetInfo()); the queued job consumes FileReader::stream(), a generator, so it holds one chunkSize() chunk and never the file; failed rows are written to the downloadable CSV as they fail. Never call FileReader::read() without a limit on a user-supplied file and never collect rows into an array to process them — that is how a million-row import becomes a fatal memory error. Spreadsheets are read in windows of spreadsheet_chunk_size rows (RowWindowFilter), first worksheet only.
  • Exact matches skip a step: Importer::isExactMatch($headers, $mapping) is true when every target column found a header AND every non-blank header was claimed — which a file filled in from the downloadable template always satisfies. The wizard then goes straight to review. It deliberately fails when a source column goes unclaimed, because that column's data would be silently dropped; the mapping step names those columns instead.
  • The dialog sizes itself, in one component: under layout: 'auto' KinetixImportModal promotes the SAME KinetixModal to fullscreen past fullscreenThreshold columns, so the wizard is resized rather than remounted and the user's mapping survives. 'sheet' is an explicit per-importer choice only — swapping shells mid-flow would unmount the wizard and lose the file.
  • The reading options are a KinetixCollapsible: the shared shadcn/Reka disclosure (a real <button> with aria-expanded, animating its measured height), collapsed by default and keeping a summary line while folded — folded is not the same as unknown. Reuse it for any disclosure; never hand-roll a v-show toggle. Its field grid uses a container query, not sm:/lg:: the dialog's width does not follow the screen, so viewport breakpoints squeezed the fields into columns their labels could not fit.
  • surface decides who scrolls: inline (the page), modal (the wizard's own capped scroller), fullscreen (its scroller fills the panel), sheet (none — KinetixSheet already scrolls). Pass it whenever you place KinetixImporter in a shell of your own.
  • Always queue heavy work: both ImportProcessor and ExportProcessor are ShouldQueue. Process in chunked DB transactions (chunkSize()), then notify via the Kinetix Notification (broadcast() when Echo is configured, else sendToDatabase()).
  • Excel vs CSV vs PDF: CSV is parsed/written natively (RFC-4180, empty escape); .xls/.xlsx go through phpoffice/phpspreadsheet; pdf renders a landscape-A4 HTML table via the optional dompdf/dompdf (install composer require dompdf/dompdf; throws a clear error if missing). All in FileWriter — add new formats there, not in the jobs.
  • Row pipeline (IMPORTANT): processRow() = prepareRow() → record resolved ONCE ($this->record($data), shared by rules()/after()/importRow() — never call resolveRecord() again in a custom importRow()) → column rules + rules() + $validateUsing (merged per field) → importRow() → castStateUsing(). Each row runs in its own savepoint (transaction_per_row, default on). ANY throwable fails that row only — never let validation or rule building escape the per-row try. Fail a row on purpose with $this->reject() / RowRejected; a bare return counts as imported. Messages name fields by column label and every message is reported.
  • Customizing: override importRow()/resolveRecord() for upsert logic, query() to scope an export, and queue()/chunkSize() for throughput — do not bypass the job.
  • Notification lifecycle is automatic: wiring ExportAction::exporter() / ImportAction::importer() gives a queued toast immediately, a completion database notification (broadcast live when Echo is configured) and a danger notification if the job dies. Rows that fail to map/validate/save are skipped, counted and reported (status warning) — never fatal — and a failed import additionally attaches a Download failed rows CSV action (every failed row + reason, signed user-bound link). Customize every message by overriding getStartedNotificationBody(), getCompletedNotificationTitle/Body(int $done, int $failed) and getFailedNotificationTitle/Body() on the exporter/importer. Completion notifications require KINETIX_DATABASE_NOTIFICATIONS=true + the notifications table + <KinetixNotifications /> mounted (or broadcasting) to be visible; they're team-stamped automatically when notifications are team-scoped.
  • Template download: the modal offers "Download template" by default (CSV of the column labels — they auto-map on upload). Opt out with protected bool $downloadableTemplate = false; rename with protected ?string $templateFileName (default = studly class name .csv; .csv is appended when an override omits it — read the name via getTemplateDownloadName()). The template starts with a UTF-8 BOM (Excel/Windows) and FileReader skips a BOM on upload. Endpoint GET {prefix}/imports/template?importer={token}.
  • Multi-tenancy (IMPORTANT): the queued job has no request — never infer the tenant inside importRow() (e.g. Team::first() is a cross-tenant leak). Override context(Request $request): array to capture ['team_id' => $request->user()?->currentTeam?->getKey()] at dispatch; it is serialized with the job and restored before any row — read it via $this->context['team_id'] / getContext(). Exporters have the same hook: scope query() with $this->context['team_id'] — an unscoped query() exports every tenant's rows.
  • i18n & docs: any new option/label must be added to every shipped locale under resources/lang/*/kinetix.php and to docs/import-export.md.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle