tourze/doctrine-direct-insert-bundle
A Symfony bundle that adds high-performance direct insert support to Doctrine, letting you bypass ORM overhead when bulk-loading rows. Simple service integration for fast, efficient writes.
Read this first: this is a Symfony bundle, not a Laravel package. The Bundle suffix, the tourze/* vendor, and the Doctrine dependency all point to Symfony's bundle registration model. Laravel has no bundle autodiscovery, no config/bundles.php, and no Kernel::registerBundles(). So "getting started" in a Laravel app means one of two paths:
tourze/doctrine-direct-insert (or similarly named) core package. Install that, skip the Symfony wiring.Connection built from Laravel's existing PDO.composer require tourze/doctrine-direct-insert-bundle:^1.0
Pin to ^1.0 rather than *. The release notes for 1.0.0 contain nothing but a compare link between 0.0.1 and 1.0.0, so treat the 0.x → 1.0 jump as possibly breaking until you have read the diff yourself: https://github.com/tourze/doctrine-direct-insert-bundle/compare/0.0.1...1.0.0.
Where to look first, in order:
composer.json of the package — check whether it declares symfony/framework-bundle as a hard requirement. If it does, installing it in Laravel drags in Symfony kernel machinery you don't want.DependencyInjection/*Extension.php and Resources/config/services.yaml — this tells you the real service ID and constructor arguments, which is what you actually need to wire manually.Connection::insert() internally.First use case: high-volume append-only writes — audit/log tables, telemetry, staging imports, CSV ingestion. The value proposition of "direct insert" is skipping the ORM's UnitOfWork: no entity hydration, no change-set computation, no per-entity flush() bookkeeping. If your write path is 10k+ rows into a table with no business logic, that overhead dominates.
Sanity-check the win before adopting, with a throwaway benchmark against plain Laravel DB::table('x')->insert($chunk). Laravel's query builder already bypasses Eloquent entirely, so the bundle may be solving a problem you don't have. Adopt it only if it offers something the query builder doesn't — typed column mapping, automatic chunking, or driver-specific multi-row SQL.
Wire the connection from Laravel, don't create a second one. The single most common integration bug is constructing a fresh DBAL connection from config, which gives you a separate PDO — separate transaction scope, separate DB::listen() invisibility, separate connection-pool slot.
// AppServiceProvider::register()
$this->app->singleton(DirectInserter::class, function ($app) {
$pdo = $app['db']->connection()->getPdo();
return new DirectInserter(new Doctrine\DBAL\Connection([], $pdo));
});
Reusing Laravel's PDO means DB::transaction() and the inserter share a transaction, and Laravel's query log still has a chance of seeing the statements.
Keep the boundary explicit. A workable convention: Eloquent owns reads and low-volume writes with business rules; the direct inserter owns bulk append-only writes only. Enforce it by putting all direct-insert calls behind a small repository class per table, so a grep for the inserter tells you every place ORM behaviour is bypassed.
Chunk, and chunk by parameter count, not row count. Multi-row inserts hit hard driver limits: PostgreSQL caps bind parameters at 65535 per statement, SQLite at 999 (older builds), and MySQL at max_allowed_packet. With 8 columns per row, 1000 rows is 8000 parameters — fine on Postgres, still fine on MySQL, but a 50k-row single statement will fail somewhere. Compute chunk size from column count:
$columnsPerRow = count($rows[0]);
$chunk = (int) floor(60000 / max(1, $columnsPerRow));
foreach (array_chunk($rows, $chunk) as $batch) {
// insert $batch
}
Wrap the whole load in one transaction where the driver allows it. For MySQL InnoDB and Postgres, a single transaction around all chunks is dramatically faster than autocommit per chunk, and gives you all-or-nothing semantics for an import. For very large loads, consider LOAD DATA / COPY instead — a direct-insert helper is not a bulk loader and won't beat the native paths.
Fill in what the ORM was doing for you. Direct insert bypasses created_at/updated_at, $casts, UUID/ULID generation, observers, and booted() hooks. Do it at the call site:
$now = now();
$rows = array_map(fn ($r) => $r + ['created_at' => $now, 'updated_at' => $now], $rows);
Queue workers: build the inserter inside the job, not as a constructor-injected singleton captured at boot, if you rely on DB::reconnect() after long idle periods — otherwise you hold a stale PDO across a MySQL wait_timeout.
0.0.1 → 1.0.0 jump signals an API-freeze decision, and freezes are where authors rename things. Read the diff before upgrading; assume the service ID or constructor signature changed until proven otherwise.laravel/framework-style package discovery, a Symfony bundle ships no Laravel provider, so there is nothing to autoregister. You are writing the wiring yourself, every time.symfony/framework-bundle is a hard require, you're adding a chunk of Symfony's DI container to a Laravel app that already has one. Check composer why symfony/framework-bundle after install; if it appears and you didn't want it, this package is the wrong tool.$fillable protection, no cast round-trip, no soft-delete scoping, no optimistic locking. If anything downstream relies on an observer firing (search indexing, webhook dispatch, cache invalidation), direct insert will silently break it. Audit observers on the target table before adopting.Schema::getColumnListing($table).true/1/'t', empty strings vs null, and DateTime objects all round-trip differently across MySQL, Postgres, and SQLite. If your test suite uses SQLite in-memory and production is MySQL, direct-insert code is exactly the layer where that mismatch bites. Run integration tests against the production engine.RETURNING, MySQL wants lastInsertId() (only reliable for single-row or with LAST_INSERT_ID(expr) tricks). Don't assume the helper hands you IDs for a batched insert.DB::listen(), and QueryExecuted events won't fire for these statements — you lose query visibility at precisely the layer with the most volume. Reuse Laravel's PDO (above) or attach a DBAL middleware/logger.Connection factory service, hook there rather than subclassing the inserter. Subclassing ties you to internals that just changed at 1.0.0.DB::table(...)->insert($chunked) is already ORM-free, already chunkable, already logged, and already covered by every debugging tool you have. The bundle earns its place only if it adds typed mapping or driver-specific optimisations that measurably beat that. Benchmark first; the dependency cost is real and the changelog gives you nothing to justify it.How can I help you explore Laravel packages today?