#
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 (requirescompareTimestamp()).
->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.
- Takes the value from
$sourceColumn. - Searches
$relatedModelwhere$relatedKeymatches that value. - If found, assigns the ID to the foreign key of
$relationName. - If
createIfMissingistrueand 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 callingatomic().TransactionMode::ROW: Wraps each individual row in its own transaction.
use LaravelIngest\Enums\TransactionMode;
->transactionMode(TransactionMode::ROW)