joy/voyager-import
Voyager Import brings flexible CSV and Excel importing to Laravel Voyager admin panels. Map uploaded columns to BREAD models, validate each row, and bulk-insert records through a clean, configurable interface built for everyday admin use.
Minimal steps
composer.json on the repo against your laravel/framework and tcg/voyager versions before committing; the last release (Feb 2024) targets the Voyager 1.5/1.6 era.composer require joy/voyager-importsrc/Providers/*ServiceProvider.php and read publishes() — you want the config file and any migrations):
php artisan vendor:publish --provider="Joy\VoyagerImport\VoyagerImportServiceProvider"
php artisan migrate
Where to look first
README.md → install + screenshot of the import flow.src/ → the service provider (what loads routes/menu/views), routes/, Http/Controllers/, and any Imports/ or Services/ directory. That tells you whether it wraps maatwebsite/excel or rolls its own reader — this drives everything else.config/ → file size limits, chunk size, allowed extensions, and column-mapping defaults.First use case
Start with the least risky table you own: a lookup/reference table (categories, tags, countries) with 2–3 columns and a unique slug. Do not start with users or anything with password hashing, polymorphic relations, or media columns. Export 3 existing rows from Voyager, re-upload the same file as your first import, and verify it round-trips (mapping, types, timestamps) before touching real data.
Pattern 1 — Admin UI import (the 90% path)
The intended workflow is: define the BREAD in Voyager → browse to the list view → click Import → upload CSV/XLSX → map file columns to BREAD fields → submit. The package typically maps strictly by column header name to the BREAD field name, so your header row should mirror the BREAD keys exactly (title, slug, status, category_id). Prefer this path for one-off data loads by ops staff; keep a sample template file in storage/app/templates/ and hand it out so headers never drift.
Pattern 2 — Programmatic import (seeders, artisan, migrations of legacy data)
For anything you'll do more than twice, don't click. Wrap the package's importer class in your own artisan command so it's versioned and repeatable:
// app/Console/Commands/ImportProducts.php
public function handle()
{
$rows = Excel::toCollection(null, $this->argument('file'))->first(); // or the package's reader
foreach ($rows as $i => $row) {
$data = $this->map($row->toArray());
$v = Validator::make($data, Product::importRules());
if ($v->fails()) { $this->failures[$i + 2] = $v->errors()->all(); continue; }
Product::updateOrCreate(['sku' => $data['sku']], $data);
}
}
Key habit: updateOrCreate keyed on a natural unique column, never insert, so re-running is idempotent. Chunk with ->chunk(500) and commit per chunk — a single transaction over a 50k-row file will hold locks and blow max_execution_time.
Pattern 3 — Row-level validation with a failure report
Collect failures rather than aborting on the first bad row. Return a CSV of {row_number, errors, raw_row} to the user. Import tooling lives or dies on this: operators will upload a spreadsheet with one bad date in row 8,431 and need to know exactly which row.
Pattern 4 — Queued imports for large files
Above ~2–5k rows, move the upload to a job. Store the uploaded file, create an ImportLog record (status, total, processed, failures), dispatch ProcessImport, and poll/echo progress. This sidesteps web timeout limits and lets you retry. The package's controller usually does the work synchronously — subclass or replace it via a route/container binding rather than editing vendor code.
Pattern 5 — Per-model importer classes
If the package exposes an importer contract, implement one per BREAD (ProductImporter, CategoryImporter) with rules(), mapRow(), beforeSave(), afterSave(). Centralizes the "how do we handle this column" knowledge instead of scattering casts across the UI config.
Integration tips
$casts, mutators, and booted() hooks do fire. Use them — a slug mutator means you can omit slugs from the CSV.resolveCategory() step in mapRow() before save.required with no default, either exclude them from import or set defaults in beforeSave().DB::transaction per chunk, not per file; log the chunk's byte offset/row range so a failed run resumes rather than restarts.File & environment
upload_max_filesize / post_max_size / max_execution_time will bite before the package does. Check php -i | grep -E 'upload_max|post_max'; a 5MB default silently rejects a 12MB XLSX with a generic validation error.\xEF\xBB\xBF to your first header, so title never matches. Strip it ($first = preg_replace('/^\xEF\xBB\xBF/', '', $first)) or save as plain CSV.0 in phone numbers, 16-digit IDs → scientific notation, dates in local locale. Format those columns as Text in the template, or cast explicitly on read. If the package uses maatwebsite/excel, date columns may arrive as Carbon instances or Excel serials depending on WithHeadingRow/format config.Data integrity
NULL. Validate '' → null before save, or your nullable integer columns become 0 and your unique indexes collide on ''.mapRow() (or rely on updateOrCreate keying).Debugging
php artisan config:clear; a stale compiled config is a classic "my settings didn't apply" trap.Maintenance risk & extension points
app/Imports/* classes so you can drop the package without losing business logic, and pin the version ("joy/voyager-import": "~1.0") so a composer update can't break the admin UI.saving, saved) over vendor patches — they survive upgrades and package removal.How can I help you explore Laravel packages today?