# The IngestConfig Class

The IngestConfig object is the declarative heart of your importer. It allows you to define the entire ETL (Extract, Transform, Load) process in a fluent, readable way.

All configuration happens inside the getConfig() method of your importer class.

# Basic Setup

# for(string $modelClass)

Required. Initializes the configuration for a specific Eloquent model. This must be the static entry point.

IngestConfig::for(\App\Models\Product::class)

# fromSource(SourceType $type, array $options = [])

Required. Defines where the data comes from.

  • $type: An enum instance of LaravelIngest\Enums\SourceType.
  • $options: An associative array of options required by the specific source handler (e.g., path, disk, url).
->fromSource(SourceType::FTP, ['disk' => 'ftp-disk', 'path' => 'import.csv'])

# Identity & Duplicates

# keyedBy(string $sourceColumn)

Defines the "Unique ID" column in your source file (not the database column name). This is used to check if a record already exists.

// The CSV has a column "EAN-Code" which is unique
->keyedBy('EAN-Code')

# onDuplicate(DuplicateStrategy $strategy)

Defines behavior when a record with the keyedBy value is found in the database.

  • DuplicateStrategy::SKIP: (Default) Do nothing. Keep the old record.
  • DuplicateStrategy::UPDATE: Overwrite the database record with new data.
  • DuplicateStrategy::FAIL: Stop processing this row and mark it as failed.
  • DuplicateStrategy::UPDATE_IF_NEWER: Only update if the source data is newer (requires compareTimestamp()).
->onDuplicate(DuplicateStrategy::UPDATE)

# compareTimestamp(string $sourceColumn, string $dbColumn = 'updated_at')

Used with DuplicateStrategy::UPDATE_IF_NEWER. Compares a timestamp from the source data with a database column to determine if the record should be updated.

->onDuplicate(DuplicateStrategy::UPDATE_IF_NEWER)
->compareTimestamp('last_modified', 'updated_at')

# Mapping & Transformation

# map(string|array $sourceColumn, string $modelAttribute)

A 1:1 copy from source to database. Supports column aliases for files with varying headers.

// Simple mapping
->map('First Name', 'first_name')

// With aliases - first match wins
->map(['email', 'E-Mail', 'user_email'], 'email')
->map(['name', 'full_name', 'Name'], 'name')

# mapAndTransform(string|array $sourceColumn, string $modelAttribute, callable $callback)

Transforms the value before saving. Also supports column aliases.

  • Callback Signature: fn($value, array $row)
  • $value: The value of the specific column.
  • $row: The entire raw row array (useful for combining columns).
// Combine First and Last name
->mapAndTransform('Last Name', 'full_name', function($value, $row) {
    return $row['First Name'] . ' ' . $value;
})

// Format currency
->mapAndTransform('Price', 'price_in_cents', fn($val) => (int)($val * 100))

// With aliases
->mapAndTransform(['status', 'Status', 'STATE'], 'is_active', fn($val) => $val === 'active')

# relate(string $sourceColumn, string $relationName, string $relatedModel, string $relatedKey, bool $createIfMissing = false)

Automatically resolves BelongsTo relationships.

  1. Takes the value from $sourceColumn.
  2. Searches $relatedModel where $relatedKey matches that value.
  3. If found, assigns the ID to the foreign key of $relationName.
  4. If createIfMissing is true and no match is found, creates the related record automatically.
// Source: "Category: Smartphones"
// Database lookup: Category::where('name', 'Smartphones')->first()
// Result: $product->category_id = $foundCategory->id
->relate('Category', 'category', \App\Models\Category::class, 'name')

// Auto-create missing categories
->relate('Category', 'category', \App\Models\Category::class, 'name', createIfMissing: true)

# relateMany(string $sourceField, string $relationName, string $relatedModel, string $relatedKey = 'id', string $separator = ',')

Automatically resolves BelongsToMany relationships. Parses a delimited string from the source and syncs the pivot table.

// Source: "Tags: PHP, Laravel, Backend"
// Splits by comma, looks up each tag, syncs the pivot table
->relateMany('Tags', 'tags', \App\Models\Tag::class, 'name', ',')

# Validation

# validate(array $rules)

Applies Laravel validation rules to the incoming data before it is transformed or saved. Keys must match the source file columns.

->validate([
    'EAN-Code' => 'required|numeric|digits:13',
    'Price' => 'required|numeric|min:0',
])

# validateWithModelRules()

Merges validation rules defined in the target model's static getRules() method. Useful for DRY (Don't Repeat Yourself). Rules from validate() take precedence over model rules.

// In Product.php
public static function getRules(): array
{
    return [
        'sku' => 'required|string',
        'name' => 'required|min:3',
    ];
}

// In Config
->validateWithModelRules()
->validate(['price' => 'required|numeric']) // Additional rules

# Hooks

# beforeRow(callable $callback)

Executed before validation. Allows you to modify the raw data array by reference. Perfect for cleaning up messy data globally.

->beforeRow(function(array &$data) {
    // Remove invisible characters from all keys
    $data = array_combine(
        array_map('trim', array_keys($data)), 
        $data
    );
})

# afterRow(callable $callback)

Executed after the model has been successfully saved.

  • $model: The saved Eloquent model.
  • $row: The original raw data.
->afterRow(function(Product $product, array $row) {
    // Sync tags or trigger side effects
    $product->search_index_updated_at = now();
    $product->saveQuietly();
})

# Processing Options

# setChunkSize(int $size)

Determines how many rows are processed per background job. Default: 100.

  • Increase for simple inserts to reduce queue overhead.
  • Decrease for memory-heavy operations (e.g., image processing in afterRow).

# atomic()

Wraps each chunk in a Database Transaction. If one row in the chunk fails, all rows in that chunk are rolled back.

  • Default: Disabled (Rows are committed individually).

# setDisk(string $disk)

Overrides the default filesystem disk (from config/ingest.php) for this specific importer.

->setDisk('s3_private_bucket')

# strictHeaders(bool $strict = true)

Enables strict header validation. When enabled, the import will fail immediately if any mapped source column is missing from the file headers. By default, only the keyedBy column is validated.

->strictHeaders()
->map('email', 'email')      // Must exist in source file
->map('name', 'name')        // Must exist in source file

# Dynamic Model Resolution

# resolveModelUsing(callable $callback)

Allows you to dynamically determine which Eloquent model to use based on the row data. This is useful when importing heterogeneous data into different tables.

  • Callback Signature: fn(array $rowData): string
  • Returns: A fully qualified model class name.
use App\Models\{User, AdminUser, Customer};

IngestConfig::for(User::class)
    ->resolveModelUsing(function(array $row) {
        return match($row['user_type'] ?? 'user') {
            'admin' => AdminUser::class,
            'customer' => Customer::class,
            default => User::class,
        };
    })
    ->map('email', 'email')
    ->map('name', 'name');

Note: The base model class passed to IngestConfig::for() is used as a fallback if no resolver is set.


# Transaction Modes

# transactionMode(TransactionMode $mode)

Fine-grained control over database transaction behavior.

  • TransactionMode::NONE: No transactions (default). Each row is committed individually.
  • TransactionMode::CHUNK: Wraps each chunk in a transaction. Same as calling atomic().
  • TransactionMode::ROW: Wraps each individual row in its own transaction.
use LaravelIngest\Enums\TransactionMode;

->transactionMode(TransactionMode::ROW)