Importers

Lifecycle Hooks

Hook into the import process at every stage with lifecycle methods.

Tapix exposes lifecycle hooks that let you run custom logic at specific points during the import process. These hooks are defined as methods on your importer class.

Hook Overview

The import lifecycle flows through these stages:

  1. Access check -- canImport() determines whether the user can access the importer.
  2. Pre-import -- beforeImport() runs once before processing begins.
  3. Pre-validation -- beforeValidation() runs before column validation starts.
  4. Per-row processing -- For each row: prepareForSave() transforms data, beforeSave() runs before persistence, then afterSave() runs after persistence.
  5. Post-import -- afterImport() runs once after all rows are processed.

Hook Reference

canImport(): bool

Controls access to the importer. Called before the import wizard is shown. Return false to prevent the user from accessing this importer.

Defaults to true.

app/Importers/ContactImporter.php
public function canImport(): bool
{
    return auth()->user()->hasPermissionTo('import contacts');
}

beforeImport(Import $import): void

Called once before the import begins processing rows. Use this to set up state, log the start of an import, or send notifications.

app/Importers/ContactImporter.php
use Tapix\Core\Models\Import;

public function beforeImport(Import $import): void
{
    logger()->info("Starting import #{$import->id} with {$import->total_rows} rows");
}

beforeValidation(Import $import): void

Called before column validation starts. Use this to prepare validation context or modify import settings before values are checked.

app/Importers/ProductImporter.php
use Tapix\Core\Models\Import;

public function beforeValidation(Import $import): void
{
    // Pre-load lookup tables for custom validation rules
    cache()->put(
        "import:{$import->id}:valid-codes",
        ProductCode::pluck('code')->toArray(),
        now()->addHour(),
    );
}

prepareForSave(array $data, ?Model $existing, array &$context): array

Transforms the validated row data before it is saved to the database. This hook runs for every row.

Parameters:

  • $data -- The validated row data as a key-value array.
  • $existing -- The matched Eloquent model instance when updating an existing record, or null when creating a new record.
  • $context -- A reference array containing import metadata. Includes tenant_id and creator_id.

The base implementation strips the id field from the data array. Always return the modified data array.

The $context array contains tenant_id (the current tenant's ID when multi-tenancy is enabled) and creator_id (the authenticated user's ID who initiated the import). Use these to scope records and track ownership.
app/Importers/ContactImporter.php
use Illuminate\Database\Eloquent\Model;

public function prepareForSave(array $data, ?Model $existing, array &$context): array
{
    // Set tenant scoping
    $data['tenant_id'] = $context['tenant_id'];

    // Set default values for new records
    if (! $existing) {
        $data['status'] = 'pending';
        $data['created_by'] = $context['creator_id'];
    }

    // Normalize data
    $data['email'] = strtolower($data['email'] ?? '');

    return $data;
}

beforeSave(Model $model, array $data): void

Called before each row is persisted to the database. The model instance has already been populated with the prepared data but has not been saved yet.

app/Importers/ContactImporter.php
use Illuminate\Database\Eloquent\Model;

public function beforeSave(Model $model, array $data): void
{
    if (! $model->exists) {
        $model->uuid = (string) Str::uuid();
    }
}

afterSave(Model $model, array $data): void

Called after each row is persisted to the database. The model has been saved and has a primary key. Use this for post-save side effects like syncing related data or dispatching events.

app/Importers/ContactImporter.php
use Illuminate\Database\Eloquent\Model;

public function afterSave(Model $model, array $data): void
{
    // Sync tags if present in the import data
    if (! empty($data['tags'])) {
        $tagIds = Tag::whereIn('name', explode(',', $data['tags']))->pluck('id');
        $model->tags()->sync($tagIds);
    }

    // Dispatch an event for downstream processing
    ContactImported::dispatch($model);
}

afterImport(Import $import): void

Called once after all rows have been processed. Use this for summary actions like sending a completion notification, updating caches, or running aggregate calculations.

app/Importers/ContactImporter.php
use Tapix\Core\Models\Import;

public function afterImport(Import $import): void
{
    // Notify the user
    $import->user->notify(new ImportCompleteNotification($import));

    // Rebuild search index
    Contact::where('import_id', $import->id)
        ->searchable();

    logger()->info("Import #{$import->id} complete: {$import->processed_rows} rows processed");
}

matchableFields(): array

Defines which fields are used to match imported rows against existing records in the database. When a match is found, the existing record is updated rather than creating a duplicate.

Defaults to [MatchableField::id()].

app/Importers/ContactImporter.php
use Tapix\Core\Matching\MatchableField;

public function matchableFields(): array
{
    return [
        MatchableField::id(),
        MatchableField::email('email'),
        MatchableField::name(),
    ];
}

Practical Example

Combining multiple hooks in a single importer:

app/Importers/ContactImporter.php
namespace App\Importers;

use App\Events\ContactImported;
use App\Models\Contact;
use App\Notifications\ImportCompleteNotification;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Tapix\Core\Fields\ImportField;
use Tapix\Core\Fields\ImportFieldCollection;
use Tapix\Core\Importing\BaseImporter;
use Tapix\Core\Models\Import;

final class ContactImporter extends BaseImporter
{
    public function model(): string
    {
        return Contact::class;
    }

    public function fields(): ImportFieldCollection
    {
        return ImportFieldCollection::make([
            ImportField::make('name')->required(),
            ImportField::make('email')->required(),
        ]);
    }

    public function canImport(): bool
    {
        return auth()->user()->can('import contacts');
    }

    public function beforeImport(Import $import): void
    {
        logger()->info("Starting contact import #{$import->id}");
    }

    public function prepareForSave(array $data, ?Model $existing, array &$context): array
    {
        $data['tenant_id'] = $context['tenant_id'];
        $data['email'] = strtolower($data['email'] ?? '');

        return $data;
    }

    public function beforeSave(Model $model, array $data): void
    {
        if (! $model->exists) {
            $model->uuid = (string) Str::uuid();
        }
    }

    public function afterSave(Model $model, array $data): void
    {
        ContactImported::dispatch($model);
    }

    public function afterImport(Import $import): void
    {
        $import->user->notify(new ImportCompleteNotification($import));
    }
}
Copyright © 2026