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

Sonata Batch Entity Import Bundle Laravel Package

jgrygierek/sonata-batch-entity-import-bundle

A Symfony bundle for Sonata Admin that adds batch entity import from CSV or Excel files, with configurable field mapping, validation, and a ready-to-use import workflow that plugs directly into your admin panel.

View on GitHub
Deep Wiki
Context7

Getting Started

jgrygierek/sonata-batch-entity-import-bundle bolts a spreadsheet-import workflow onto Sonata Admin: an Import action appears on an admin's list view, you upload a CSV/XLSX file, the bundle renders a preview table of parsed rows, lets editors fix or drop individual rows, and then persists the surviving rows as Doctrine entities. It sits on top of sonata-project/admin-bundle and PhpSpreadsheet, so the target admin keeps its normal CRUD and just gains a bulk-loading entry point.

Minimal setup:

composer require jgrygierek/sonata-batch-entity-import-bundle

Register the bundle in config/bundles.php:

Jgrygierek\SonataBatchEntityImportBundle\SonataBatchEntityImportBundle::class => ['all' => true],

Give it a storage directory and sane upload limits (config/packages/sonata_batch_entity_import.yaml):

sonata_batch_entity_import:
    allowed_extensions: ['csv', 'xlsx']
    max_file_size: 5M
    root_dir: '%kernel.project_dir%/var/import'

Then opt the admin you want to import into in. The pattern is "trait + interface + field mapping":

use Jgrygierek\SonataBatchEntityImportBundle\Admin\ImportAdminInterface;
use Jgrygierek\SonataBatchEntityImportBundle\Admin\ImportAdminTrait;
use Jgrygierek\SonataBatchEntityImportBundle\Mapper\ImportMapper;

final class ProductAdmin extends AbstractAdmin implements ImportAdminInterface
{
    use ImportAdminTrait;

    protected function configureImportMapper(ImportMapper $mapper): void
    {
        $mapper->addColumn('name');
        $mapper->addColumn('price');
        $mapper->addAssociation('category');
    }
}

Clear the cache, open the admin's list view, and the Import button should be there. Upload a three-row CSV with headers name,price,category to verify the whole loop before handing it to users. Method names and the config keys have moved between releases, so copy the signature straight from the README or ImportAdminTrait of the version you actually install.

Platform baseline as of v2.8.0: PHP 8.2+ (8.1 support was dropped), with Symfony 8 added to the supported matrix alongside the existing 6.x/7.x lines. If your app or CI image is still on PHP 8.1, pin to the 2.7.x line.

Where to look first in the repo: the README, then src/Admin (trait/interface that wire the route and action), src/Mapper (how columns become entity properties), src/Controller (the upload → preview → confirm flow), and src/Resources/config (services, templates, routes). Read those four before customising anything.

Implementation Patterns

Field mapping is the whole integration. Everything else is defaults. A pragmatic mapper for a real entity:

$mapper->addColumn('sku', ['required' => true]);
$mapper->addColumn('name',  ['required' => true]);
$mapper->addColumn('price', ['type' => 'number']);
$mapper->addColumn('description');
$mapper->addAssociation('category');          // matched by identifier, not by label
$mapper->addAssociation('manufacturer');
$mapper->addCustomField('active', fn ($value) => filter_var($value, FILTER_VALIDATE_BOOLEAN));

Use a custom field / callback for every value that users type by hand. Imported data has no form layer sanitising it: trim whitespace, normalise booleans (yes/true/1/TAK), parse dates (Y-m-d vs Excel serial numbers), slugify, cast decimals with the right separator. Do this in the mapper rather than in entity setters, so the admin form and the importer stay decoupled.

Associations need a resolvable identifier. The importer resolves category by looking up an existing entity, typically from the same admin or a configured target admin. If your spreadsheet contains human labels instead of IDs/slugs, either add a custom field that queries the repository, or require users to export IDs first. Never rely on fuzzy matching — a silently wrong foreign key is the worst failure mode of a bulk import.

Reuse a base admin for repeated imports. If you import several entities, put shared configureImportMapper boilerplate (timestamps, owner, tenant) into an abstract import admin and extend it. Keep mapper configuration declarative and small; any branching logic belongs in custom fields.

Attach context with events, not setters. Use the bundle's pre/post-import events (or an import-specific Doctrine listener) to stamp createdBy, tenant/shop ID, import source, or to fire notifications. Note that standard Doctrine lifecycle callbacks do run because entities are persisted through the EntityManager, but Sonata admin form events do not — anything that lives in PRE_SUBMIT/POST_SUBMIT handlers will be skipped.

Ship a template file. Generate a download-able CSV with exactly the headers your mapper expects (and a couple of valid example rows) on the same screen or in the admin's docs. Most import failures are header typos and missing association identifiers, and a template removes both.

Treat preview-and-edit as the review step, not the validation step. The preview is where editors catch bad rows; server-side validation still has to run on final persist. Add Symfony Validator constraints on the entity, and if constraints are conditional (e.g. "required only when publishing"), make sure the import path sets enough state to satisfy them — or the whole import aborts on rows that are otherwise fine.

Test with fixtures, not by hand. Keep small tests/fixtures/*.csv files covering: all-valid, missing association, duplicate identifier, empty optional fields, and a UTF-8 file with a BOM. Run them through the import flow in a functional test (or a small CLI script that reuses the mapper) so a bundle upgrade that changes parsing behaviour gets caught immediately.

Gotchas and Tips

  • Small package, single maintainer, ~zero dependents. Pin the version in composer.lock, read the diff before upgrading, and be prepared to vendor/fork. Recent release date is good; the ecosystem footprint is not a safety net.
  • Track the compatibility matrix, not just the version number. v2.8.0 dropped PHP 8.1 and added Symfony 8 support, so the supported floor moved without any API change. Before bumping, run composer why-not and check your CI matrix — a green local build on PHP 8.1 will fail after the upgrade. Conversely, if you're planning a Symfony 8 migration, 2.8.x is the first line to target.
  • CSV encoding and delimiters are the #1 support ticket. Enforce UTF-8, strip the BOM, and document that Excel's default ; delimiter in some locales will break parsing. Prefer XLSX if your users are Excel-native, but then watch date cells — PhpSpreadsheet hands back serial numbers, so date columns need an explicit conversion.
  • Header matching is literal. Rename a column in the mapper and every existing user template silently fails. Keep header names stable and match them exactly, including case and spacing.
  • Upload limits are enforced twice. The bundle's max_file_size and PHP's upload_max_filesize/post_max_size are independent; an oversized POST returns an empty request rather than a friendly error. Raise both together.
  • The storage directory must exist and be writable by the web user. Uploaded files are staged there between the preview and the confirmation step — check var/import (or your configured root) first when the preview is empty or the confirm step 404s.
  • No upsert by default. Assume import inserts new rows. If you need "update if SKU exists", implement it explicitly with a custom field plus a repository lookup, or run a dedupe pass afterwards. Verify against the mapper options of your installed version before promising this to users.
  • Partial-failure semantics are unclear until you test them. Wrap the import in a transaction or verify per-row error handling. A 500 on row 400 of 500 should not be a mystery to the person who uploaded the file.
  • Large files will hit memory and max_execution_time. PHP array-based parsing of a 20k-row XLSX is not free. For big loads, push the import to a Messenger job or a console command that reuses the mapper, and keep the web-based flow for hundreds of rows.
  • Debug the wiring with bin/console. debug:router | grep -i import confirms the route exists; debug:container | grep -i import confirms your admin is tagged/recognised. A missing button is almost always the interface not implemented on the admin class.
  • Check Sonata and Symfony constraints before adopting. This bundle tracks Sonata Admin 4.x/5.x and Symfony 6.4/7.x/8.x on PHP 8.2+; an older Sonata or a PHP 8.1 host will either fail to resolve or break the preview form. Confirm composer why-not is clean.
  • Permissions are inherited from the admin. The import action should be gated by the admin's access checks — make sure your editor role doesn't get a bulk-write capability they don't have on the normal form. If the action isn't gated, wrap it with checkAccess('EDIT') in an overridden action.
  • Log the raw row on failure. When a row fails validation, the user needs to know which row and which column. Add logging or a custom error handler around the import service; the default exception message often lacks the spreadsheet row number.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource