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

Import Engine Bundle Laravel Package

mathielen/import-engine-bundle

A Symfony bundle for building configurable data import pipelines. Define import definitions, readers, processors, and writers, then run imports from services or the console with reusable mapping and validation.

View on GitHub
Deep Wiki
Context7

Getting Started

mathielen/import-engine-bundle is a Symfony bundle from the Symfony 2.x/3.0 era, not a Laravel package. Treat it as a library you bolt on, not something php artisan vendor:publish will wire up for you. There is no service provider, no facade, no config file Laravel will pick up. The 0.5 release adds Symfony 3.0 compatibility, a static-context feature, a default trim converter and a console command — useful, but it does not change the integration story.

Install and inspect first:

composer require mathielen/import-engine-bundle:^0.5

Before writing any code, read these three things in the vendor directory, in this order:

  1. Resources/config/services.xml — this is your real API. It lists the service IDs (import_engine, readers, writers, transformers, and as of 0.5 mathielen_importengine.converter.trim), plus constructor arguments. Everything else is commentary.
  2. Tests/ — the bundle's own functional tests are the only working documentation of the definition annotation syntax, the static-context API shape, and the result object.
  3. Definition/, Transformer/, Reader/, and the new Command/ directory — the extension seams you will actually touch.

Minimal first use case — import a CSV of contacts and upsert them, without worrying about Doctrine:

// A plain DTO the engine writes into via PropertyAccess.
class ContactRow
{
    public $email;
    public $name;
    public $company;
}

Define the mapping as a definition class (annotations, so doctrine/annotations must be installed and the class must be PSR-4 autoloadable — put it in app/Imports/). Then drive the engine directly rather than through the container:

$definition = $container->make(ContactImportDefinition::class);
$definition->setStaticContext(['run_id' => $run->id, 'tenant' => $tenantId]); // 0.5

$result = $engine->import($definition, storage_path('imports/contacts.csv'));

foreach ($result->getErrors() as $error) {
    // $error carries the source row number + the offending field
}

Wire the engine in an AppServiceProvider with a small factory that hands it a CSV reader, a writer of your choosing, the transformer registry (including the built-in trim converter), and the annotation reader. Roughly 40 lines and you own the lifecycle.

First milestone: one CSV, ten rows, printed to a log, no database writes. Get the definition/annotation plumbing working before you introduce persistence — the annotation reader is the part most likely to fight you. Use the static context to stamp the run ID so your debug logs are already useful.

Implementation Patterns

One definition class per import shape, not per file. A definition describes the mapping contract (target fields, sources, transformers, identification key). Keeping them in app/Imports/ and versioning them alongside the code means an import format change is a reviewable diff.

Use the static context for per-run metadata, not for row data. The 0.5 addition lets you attach a scalar or array to the profile that transformers and writers can read. It is the right place for run_id, tenant/organisation ID, an acting user, a source system tag, or a "mode" flag (dry_run, import, update_only). It is the wrong place for anything row-specific, and it must be JSON-serialisable because your definition travels into the queued job.

Readers are the cheapest thing to replace. The bundled CSV reader is fine; the Excel reader drags in PHPExcel, which is dead. If you need XLSX, read it with phpoffice/phpspreadsheet in a small adapter and hand the engine an array/iterator. Do not inherit the bundle's Excel path.

Transformers carry the messy normalisation. Register one per concern — lowercase, phone_e164, date_from_format. As of 0.5, trimming is built in (mathielen_importengine.converter.trim); put it first in every chain rather than reimplementing it, and delete any homegrown trim transformer so you don't double-apply. Keep them small, individually unit-testable, reusable across every import. This remains the highest-value part of the package and the part most worth copying wholesale if you later drop the bundle:

class PhoneNormalizer implements TransformerInterface
{
    public function transform($value, ImportContext $context = null)
    {
        return preg_replace('/[^0-9+]/', '', (string) $value);
    }
}

Upsert via identification, never via "insert everything and dedupe later." Point the definition's identifier at a normalised, indexed column (email, external SKU, ISO country code). Rows without a resolvable identifier should fail loudly into the result's error collection rather than creating invisible duplicates.

Validate before you write. Run Laravel's validator inside a custom transformer stage or immediately after the row is materialised, and push failures into the result instead of throwing. An import that aborts on row 4,000 of 10,000 is worse than one that reports 37 bad rows and 9,963 successes.

Chunk and flush. Read in batches of 500–1000, wrap each batch in its own transaction, and record progress in an import_runs table (id, filename, total, succeeded, failed, started_at, finished_at). Pass the run ID in via static context so transform-level logging is traceable. That table is what your UI and your support team will actually use.

Drive it from a queued job, not a controller. ImportContactsJob receives the stored file path and the definition class name, builds a fresh definition (never a shared singleton), sets the static context, runs the engine, writes the summary. Controllers just accept the upload and dispatch. This is the single biggest day-to-day ergonomics decision.

Field format guessing is convenience, not a contract. 0.5 improves format guessing for the ValueObject generator. If you use that path, verify the guesses against a sample of real files — a guessed date format silently flips day/month on a subset of rows. For anything that matters, declare the format explicitly in the transformer.

Profile discovery. importengine:list shows registered import profiles. It is a Symfony console command, so it will not appear in php artisan list — either register it manually in routes/console.php/a kernel binding, or just write a 20-line artisan command that enumerates your app/Imports/ definitions. In a Laravel app the artisan command is usually the lower-friction answer.

Mirror the definitions for export. The bundle's symmetric export side reads the same mapping metadata. If you only ever import, ignore it — but if you have both directions, one definition serving both keeps the column order honest.

Laravel glue: bind the engine as a singleton with an explicit constructor (readers, transformers, annotation reader, writer). Do not let Symfony's autowiring magic leak into your container — you want a factory function you can read in one screen. Bind definitions as transient, not singleton, now that they carry mutable static context.

Gotchas and Tips

It is effectively abandoned. The 0.5 release bumps Symfony to 3.0, but that is still a 2015-era dependency tree and the package remains a low-usage, single-maintainer artefact. On a modern Laravel app you will hit constraint conflicts with Symfony 6/7. Resolve them by (a) forking and loosening constraints, or (b) copying the definition/transformer/engine classes you need into app/Import/ and dropping the bundle. Option (b) is usually cheaper and is the honest recommendation for anything long-lived.

Static context is mutable state on the definition. Because it lives on the profile object, a container-shared instance will leak one job's tenant or run ID into the next. Always make() a fresh definition per run, set the context last, and keep the payload to scalars/arrays. If a transformer mutates the context array, you get cross-row bleed that is very hard to see.

Doctrine assumptions are baked into the writers. The entity-importer path expects a Doctrine ObjectManager. In Laravel you either supply an Eloquent-backed writer implementing the bundle's writer interface, or you write into plain DTOs and persist yourself. The second path is far less work and keeps the engine ignorant of your ORM.

Definitions are cached. Annotation-parsed definitions get cached; editing a mapping and seeing no behaviour change is the classic first-hour bug. Clear the cache, and check whether the cache directory is configurable at all.

Failures are collected, not thrown. import() returning normally tells you nothing. Every caller must inspect getErrors() / the failure collection and act on it. Log the whole result at info with the run ID from the static context — you will be grateful at month three.

CSV specifics that bite: byte-order marks on the first header, trailing whitespace in header names, \r\n inside quoted fields, and Windows-1252 files from finance departments. Set delimiter, enclosure, and encoding explicitly on the reader; never rely on sniffing. The new default trim converter cleans up cell values but does not clean header names — do that yourself before handing the file over.

Empty string is not null. Transformers receive '' for missing cells, and the trim converter makes whitespace-only cells collapse to '' more often than before. Decide once, globally, whether '' means "skip this field" or "set null", and enforce it in a base transformer. Inconsistent handling produces half-populated records that are painful to backfill.

Memory: Excel readers load the whole sheet. CSV readers that file_get_contents do the same. For anything above a few thousand rows, use a streaming reader or a chunked iterator — the engine's batching helps only if the reader itself is lazy.

Extension points worth knowing: custom transformers (tag/register them, trim first), custom readers implementing the reader interface (your best lever for weird formats), the identification strategy (override it when "match on email, but fall back to external ID" is a business rule), and the static context (per-run flags and tenant scoping). Everything else is easier to do outside the engine than inside it.

Debugging trick: when a row silently does nothing, dump the materialised object before the writer runs, including the static context. Nine times out of ten the mapping matched but a transformer returned null — or a trim-then-validate chain turned a padded value into '' and your writer skipped it. A one-line Log::debug(json_encode([$context, $row])) in the loop saves an hour of annotation archaeology.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle