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

Flusher Laravel Package

endroid/flusher

Endroid Flusher auto-tunes database batch sizes when importing or modifying large data sets. It incrementally tries new sizes, keeps the fastest, and switches if conditions change. Create with Flusher($manager), call flush() while persisting, and finish() to flush remaining data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require endroid/flusher

The package is a thin layer over Doctrine ORM, so you need a working EntityManagerInterface first. There is no bundle, no service provider, and no auto-discovery — it is a plain library you instantiate yourself.

Where to look first

  1. src/ — the entire library is a handful of classes; read the Flusher class signature before wiring it anywhere. The public API is small enough to read in a few minutes and this is the fastest way to confirm the exact method names (persist() / flush() / constructor arguments) for the version you pulled in.
  2. tests/ — endroid packages are usually well tested; the test cases are the best documentation of the intended loop shape.
  3. README.md — explains the why (adaptive batch size) but not much of the how-to-integrate.

First use case

Replace the hand-rolled counter idiom in an import command:

// Before: a magic number you guessed, and it's wrong on someone else's hardware
foreach ($rows as $i => $row) {
    $em->persist($this->hydrate($row));
    if ($i % 250 === 0) {
        $em->flush();
        $em->clear();
    }
}
$em->flush();

// After: let the flusher decide
$flusher = new Flusher($em /*, step size */);
foreach ($rows as $row) {
    $flusher->persist($this->hydrate($row));
}
$flusher->flush(); // final drain

The value proposition is real but narrow: you get it only on bulk write workloads with unpredictable optimal batch sizes — CSV/API imports, backfills, migrations that hydrate entities, data anonymisation jobs. For a normal request that persists three entities, this is dead weight.

Implementation Patterns

Wrap it, don't scatter it. Put the flusher behind a small application-level service (BulkWriter, ImportPersister) so your import code deals in domain objects and the tuning strategy stays swappable:

final class BulkWriter
{
    public function __construct(private EntityManagerInterface $em) {}

    public function write(iterable $entities): void
    {
        $flusher = new Flusher($this->em);
        foreach ($entities as $entity) {
            $flusher->persist($entity);
        }
        $flusher->flush();
    }
}

Keep the flusher alive for the duration of the job, not the request. Its whole mechanism is incremental — it starts small and ratchets the batch size up or down based on measured flush cost. Create one per import run, not per chunk, otherwise it never gets past its first probe.

Symfony console command pattern. This is the natural home for the package:

#[AsCommand(name: 'app:import-products')]
class ImportProductsCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $flusher = new Flusher($this->em);
        foreach ($this->reader->rows() as $row) {
            $flusher->persist($this->hydrator->hydrate($row));
        }
        $flusher->flush();
        return Command::SUCCESS;
    }
}

Register the EntityManagerInterface injection normally; instantiate the flusher inline or as a factory service.

Do not clear the EntityManager underneath it. The tempting copy-paste from old import scripts is $em->clear() after each flush to control memory. That detaches every managed entity and will break any entity you still hold a reference to (and any association you touch afterwards). If memory is genuinely the constraint, let the flusher own the flush cadence and handle detaching deliberately — or better, process in resumable pages keyed by ID so you can drop the whole EM per page.

Transaction discipline. Wrap the entire run in an explicit transaction only if the dataset fits comfortably in one — which for the workloads this package targets it usually does not. Prefer per-batch transactions (Doctrine's implicit behaviour) so a failure mid-import leaves you with a known-good prefix rather than a rollback of two hours of work.

Pair with an ID cursor, not an offset. If the flusher clears or you restart the job, OFFSET pagination will silently skip or duplicate rows as the table grows. Iterate WHERE id > :lastId ORDER BY id LIMIT :n.

Log the chosen batch size. You want observability into whether the adaptation is doing anything useful. If the class exposes the current batch size (check the source), log it periodically from your command; otherwise wrap the flusher and time each flush yourself. A run that settles on batch size 1 means the workload is not batch-friendly and the package is just adding overhead.

Use it as a template, not a dependency, if the API is too thin. At this size, it is entirely reasonable to vendor the tuning loop into your own AdaptiveFlusher and keep control of the interface. The algorithm — try a size, measure, step, keep the winner — is a dozen lines.

Gotchas and Tips

  • Maturity is the headline risk. 7 stars, 0 dependents, README-level documentation. No third-party battle-testing to lean on. Read src/ fully before production use, pin the version in composer.json, and be prepared to fork. Do not let it become load-bearing in a critical path without a fallback.
  • persist() is not flush(). Make sure your loop actually drains at the end. A silent off-by-one that leaves the tail of the dataset unflushed is the classic failure mode here, and it only shows up on datasets that aren't a clean multiple of the final batch size.
  • Timing is noisy. The adaptation measures wall-clock flush cost, so on shared CI runners, noisy neighbours, or a database with a cold buffer pool the first few measurements are garbage. Warm up, or feed it a sensible starting step rather than letting it probe from 1.
  • Dev-mode skew. With the Symfony profiler, Doctrine debug logging, or APP_DEBUG=1, every flush is far more expensive and the tuning is meaningless. Always benchmark and import with debug off and doctrine.sql_logger / profiler disabled.
  • Detached-entity traps. After any clear, previously loaded relations are uninitialised proxies. Re-fetch by ID or use $em->getReference() for foreign keys you need to set on new rows.
  • Cascade and orphan removal multiply the cost model. A flush that cascades persists across a large object graph has a completely different profile from one row per entity, so the batch size it converges on will not transfer between entities. Expect different optima per import.
  • Long-lived workers (Messenger, RoadRunner, Octane) keep state. A flusher instance held across messages carries a stale batch size and a stale EntityManager. Rebuild both per message, and reset the EM between handled messages or you get a memory leak regardless of batch size.
  • gc_collect_cycles() and memory. If you are fighting memory, note that PHP's cycle collector is not run per flush — an occasional gc_collect_cycles() in your loop is often the fix that makes a larger batch size viable.
  • Extension points. The realistic integration surface is: the constructor's step/config argument, and wrapping the class. If you need custom measurement (e.g. rows/second instead of seconds/flush), you are writing your own — which the codebase size makes easy.
  • Test with a deliberate step. In tests, fix the step size and use a small dataset so behaviour is deterministic; otherwise assertion flakiness around row counts will cost you more time than the package saves.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator