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

Doctrine Fixtures Timing Bundle Laravel Package

nicodemuz/doctrine-fixtures-timing-bundle

A Symfony bundle that times Doctrine fixture loading, helping you spot slow data fixtures during development and testing.

View on GitHub
Deep Wiki
Context7

Getting Started

First, correct the classification before you write any code. This is a Symfony bundle (-bundle suffix, registered in config/bundles.php, listening on Symfony console/kernel events). Laravel has no bundle registry, no doctrine/doctrine-fixtures-bundle in the default stack, and no shared event lifecycle with it. Do not composer require this into a Laravel app expecting it to work — you will get a package that installs cleanly and does nothing.

Where to look first. Clone the repo and read, in this order:

  1. composer.json — the real support matrix (PHP, Symfony, doctrine/doctrine-fixtures-bundle major version). The v1.0.0 release notes are an auto-generated changelog link with no body, so this file is your only compatibility source of truth.
  2. The event subscriber/listener class — the critical question is what it hooks: a Symfony console.* event, or Doctrine's fixture-load events. If it's console-only, it is portable in spirit; if it's Doctrine-internal, close the ticket.
  3. The measurement body — is it microtime()/hrtime() around fixture execution (portable, ~50–100 lines) or a DebugStack/Symfony Profiler tie-in (not portable)?

Your first use case is not adopting the package — it's answering "which of my seeders is slow?" That question is answerable natively in an afternoon. Start here:

// database/seeders/Concerns/TimesExecution.php
namespace Database\Seeders\Concerns;

trait TimesExecution
{
    protected function timeIt(string $label, callable $fn): mixed
    {
        $start = hrtime(true);
        try {
            return $fn();
        } finally {
            $ms = (hrtime(true) - $start) / 1e6;
            app(\App\Support\FixtureTimings::class)->record($label, $ms);
        }
    }
}
// database/seeders/DatabaseSeeder.php
public function run(): void
{
    $this->timeIt(static::class, fn () => parent::run());
    // or per-call:
    $this->timeIt(ProductSeeder::class, fn () => $this->call(ProductSeeder::class));
}

Then dump the report from a terminating callback on the db:seed command. That is the whole value proposition, and you own it.

Only run the vendor spike if you genuinely need the reference implementation. Timebox it to a day in a throwaway Symfony skeleton, purely to read the real instrumentation code. Do not let this gate a fixture-performance initiative.

Implementation Patterns

Pattern 1 — Base seeder class (recommended default)

Override run() once in a base class; every seeder inherits timing for free. This mirrors what the bundle does for Doctrine fixtures and is the smallest-surface integration.

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Support\FixtureTimings;

abstract class TimedSeeder extends Seeder
{
    public function __invoke(array $parameters = []): mixed
    {
        $start = hrtime(true);
        try {
            return parent::__invoke($parameters);
        } finally {
            FixtureTimings::record(
                static::class,
                (hrtime(true) - $start) / 1e6,
            );
        }
    }
}

Pattern 2 — Whole-command timing via Artisan events

Good for CI: capture total db:seed wall time and print a top-N report on CommandFinished. This is the Laravel equivalent of the bundle's console-level hook.

// app/Providers/AppServiceProvider.php (register only in local/testing)
use Illuminate\Console\Events\CommandFinished;
use Illuminate\Support\Facades\Event;

Event::listen(CommandFinished::class, function (CommandFinished $event) {
    if (! in_array($event->command, ['db:seed', 'migrate:fresh'], true)) {
        return;
    }
    FixtureTimings::report($event->output);
});

Pattern 3 — Query-count attribution alongside duration

Duration alone tells you what is slow; query counts tell you why. Pair the timer with DB::listen scoped to the seeder run.

DB::listen(function (QueryExecuted $q) use (&$stats) {
    $stats['queries']++;
    $stats['ms'] += $q->time;
});

Pattern 4 — CI regression gate

The genuinely valuable use of per-fixture timings is a threshold check, not a pretty table. Emit JSON from the timing collector and assert on total fixture time in CI:

// tests/Feature/FixturePerformanceTest.php
public function test_fixture_suite_stays_within_budget(): void
{
    $this->seed();
    $total = FixtureTimings::total();
    $this->assertLessThan(30_000, $total, "Fixture suite took {$total}ms");
}

Keep the budget loose and the assertion about totals; per-fixture assertions are flaky on shared CI runners.

Pattern 5 — Extend the package's ideas, not its class

If the Symfony source turns out to be a thin microtime wrapper, port the reporting format (sorted descending, top-N, percentage of total) rather than the code. The formatting is the part that saves you debugging time; the timing is three lines.

Workflow tips

  • Gate all of this behind app()->environment(['local', 'testing']) — never in production.
  • Default output to top 10 slowest + total, not the full list. At 20 fixtures a full list is useful; at 500 it is noise.
  • Print the report after the transaction commits, not inside it, or buffered console output can interleave with migration noise.
  • Compare against php artisan migrate:fresh --seed rather than db:seed alone — RefreshDatabase timing without migrations is misleading.

Gotchas and Tips

The bundle will not work in Laravel. Ever. It needs config/bundles.php registration and Symfony's event dispatcher. If you see a PR adding it to composer.json, that is the bug. Correct the classification at the planning stage — mid-sprint discovery is the most expensive failure mode here.

Silent non-integration is the top failure mode even in Symfony. A listener that registers but never fires produces empty output, which reads exactly like "fixtures are fast." If you port the pattern, assert the collector actually recorded N entries; don't trust an empty report.

Nested $this->call() double-counts. DatabaseSeeder::run() calling ProductSeeder means the parent's wall time includes the child's, and your report will show the parent as the bottleneck. Either instrument only leaf seeders, or record both self-time and inclusive-time and label them distinctly. The Symfony bundle has the same conceptual issue with fixture dependencies — check how it handles getDependencies().

hrtime(true) not microtime(true). You're measuring sub-millisecond fixture bodies; microtime is wall-clock and will wobble under NTP. Return nanoseconds and convert at the boundary.

RefreshDatabase distorts everything. The trait wraps each test in a transaction and runs migrations once; fixture timings inside a transactional test are not representative of a cold migrate:fresh --seed in CI. Measure CI seeding in a dedicated job, not in the test suite.

--class vs default. php artisan db:seed runs only DatabaseSeeder; --class=ProductSeeder runs one. If your instrumentation hooks the command rather than the seeder, --class runs produce a report with one entry and no nesting context. Hook the seeder, report at the command.

Query logging will dominate your memory profile. DB::listen accumulates per-query; on a 500k-row seeder this is hundreds of MB. Aggregate into counters inside the listener (as in Pattern 3) and never store the query objects.

Doctrine fixtures ≠ Eloquent factories. If your team runs laravel-doctrine, the fixture runner is still not doctrine-fixtures-bundle in the typical wiring. Verify which runner actually executes before assuming any Doctrine-domain tooling applies.

Abandonment risk is real. Single author, zero dependents, 1.0.0 shipped with no release narrative. If you somehow do take a dependency, pin ~1.0 exactly and document the fallback ("reimplement in app/Support/FixtureTimings.php") in the PR description. Never let it become load-bearing for a shared workflow.

Extension points worth stealing, not depending on: the per-fixture label format, the sorted-descending report, and any "top N" truncation. Those are UX decisions baked into ~100 lines you can copy in an afternoon under MIT.

Debugging your own collector: if the report is empty, check in this order — (1) is the timing env-gated and the env actually local/testing? (2) is the base class actually extended by the seeder being run? (3) is DatabaseSeeder::run() calling parent::run() or short-circuiting? (4) did the terminating callback register before the command finished?

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console