evotodi/seed-bundle
Symfony/Doctrine bundle for loading and unloading seed data before your initial production push. Define seed commands with ordering to populate tables like states or initial admin users, unlike DoctrineFixturesBundle, which targets development.
The honest first step is a stack check. This is a Symfony 6+/Doctrine bundle, not a Laravel package — it has no artisan integration, no service provider, no Laravel container wiring. If you are a Laravel developer, php artisan db:seed, DatabaseSeeder, and model factories already cover the entire job this bundle does, and pulling this in would drag Symfony Console + DI assumptions into your app for zero gain. Read the rest of this section as "what to do if you're standing in a Symfony codebase" or as "what to steal conceptually for your Laravel seeder."
Minimal Symfony setup:
composer require evotodi/seed-bundle (requires PHP 8+, Symfony 6+; 7.x targets Symfony 7/8).src/DataSeeds/ and register it as a service namespace in config/services.yaml:
services:
Evotodi\SeedBundle\DataSeeds\:
resource: '../DataSeeds/*'
Evotodi\SeedBundle\Command\Seed with a static seedName(), an optional getOrder(), and load() / unload() return-int methods.bin/console seed:load (all seeds, ordered) or bin/console seed:user load (one seed).Where to look first: the README's UserSeed example is the entire mental model — seedName() defines the command name (seed:user), load() persists via $this->manager, unload() clears the table. There is no config file (dropped in v6).
First use case to ship: the initial admin user + role/status lookup tables. That's the canonical "must exist before first production traffic" dataset, and it's the case this bundle was explicitly built for.
If you're porting the idea to Laravel (which is the sane move), the mapping is direct:
database/seeders/UserSeeder.php extending Illuminate\Database\SeederseedName() ordering → $this->call([...]) order in DatabaseSeederseed:load user → php artisan db:seed --class=UserSeederload() / unload() → run() + a truncate() in a down/rollback method--skip / glob filtering → not built-in; add a flag in your own commandReference-data seeds (the primary pattern). One seed class per logical dataset — country, state, currency, role — each returning a stable getOrder(). Keep data in an array or a small fixture file in the seed class; the seed should be readable in a PR diff. This is the pattern that actually pays off day-to-day: onboarding an environment becomes bin/console seed:load, not "restore this dump."
Initial-admin seed — make it idempotent even though the bundle doesn't. load() is not idempotent by design; it assumes a clean database. Guard with a lookup before persist:
if (!$this->manager->getRepository(User::class)->findOneBy(['email' => $admin])) {
// persist
}
$this->manager->flush();
$this->manager->clear();
This is what lets the same command run again in staging without exploding on a unique constraint.
Environment-partitioned seeds via glob. The bundle's genuinely nice idea is naming seeds like prod:users:us, dev:prices and filtering with webmozarts/glob (prod/users/*, prod/**/*, dev:prices prod:users:*). Colons and slashes are interchangeable. Use this to carve a production corpus apart from a demo/dev corpus without a second tool — that's the one feature Laravel's db:seed doesn't natively give you.
Ordering as a dependency graph (a weak one). getOrder() ascending is your cross-seed dependency mechanism: roles before users, countries before states. Because ties are semi-random, encode real dependencies as strictly increasing integers with gaps (0, 10, 20) — that leaves room to slot a seed in later without renumbering and without creating accidental ties.
Operator-invoked, not migration-attached. The bundle's design intent is that seeding is a deliberate operator step in the deploy runbook, not a side effect of doctrine:migrations:migrate. Wire it as a release step: seed:load --break --debug in the deploy script (the --debug output is your audit trail of what ran in what order). Note there's no transaction wrapping and no idempotency guarantee, so --break matters.
unload() is a rollback button — treat it as armed. It's typically a raw DELETE FROM/TRUNCATE. That's fine on a fresh bootstrap database and dangerous anywhere else. If you expose seed:unload in a runbook at all, gate it behind an environment check in your wrapper script; don't let it near a database with customer data.
Laravel-side integration tip if you're bridging both worlds: don't install this bundle. Reuse the conventions — one seeder per reference dataset, an explicit ordering array in DatabaseSeeder, and an environment filter using --class or a custom --group option on a small Artisan command. You get 95% of the value in code you already own.
Equal getOrder() values load semi-randomly — this is the #1 footgun. The README calls it out twice. It becomes a real bug when combined with --from: seed:load --from seed-c may or may not run seed-b/seed-d if they share an order. Always run seed:load --debug in staging before trusting a production run, and prefer unique, spaced order values over ties.
Seeds not appearing under seed: in the command list = clear the cache. Console command registration is cached; bin/console cache:clear is the standard first move.
Naming is load-bearing. seedName() must follow Symfony's colon-separated console convention, and the class must extend Seed — the bundle discovers commands, it doesn't scan for an interface.
Return codes are enforced. load()/unload() must return 0 or Command::SUCCESS; anything else is a failed seed and (with --break) halts the run. A missing return gives you null, which is not success — silent failure.
Call disableDoctrineLogging() at the top of load(). The README warns Doctrine logging eats a lot of memory; large seed inserts will balloon RSS otherwise. Also flush() then clear() per batch to keep the identity map from accumulating entities.
The admin-user example uses passwordEncoder->encodePassword(), which is deprecated in modern Symfony — use UserPasswordHasherInterface::hashPassword(). Copy-pasting the README verbatim onto Symfony 7/8 will surface this.
Version floor is real. v6.0 was a rewrite (config file dropped, seedName() static, no *Seed suffix requirement, PHP 8/Symfony 6 minimum). Any tutorial or Stack Overflow answer predating that is wrong. Releases land roughly annually — treat 7.x/8.x support as "someone checked the deprecations," not "actively developed."
Supply-chain sanity check before adopting: ~14 stars, zero dependents, effectively single-maintainer, last substantive release early 2024. It's MIT and small enough to vendor and read in an afternoon — that's the mitigation. If it must never break, fork it into your monorepo and pin; the surface area (one base command class + two global commands) is small enough that owning it is a weekend, not a quarter.
Debugging a failed seed: run the per-seed command directly (bin/console seed:user load) to isolate it from ordering/glob concerns, then seed:load --debug user to confirm it's in the registry and where it sorts. If ordering looks wrong, that's almost always a tie, not a glob problem.
Extension points worth knowing: you can skip the src/DataSeeds/ service convention entirely and register seeds like any other command service; the glob filter accepts full webmozarts/glob syntax, so ** recursive patterns are available for grouping by environment or feature area.
How can I help you explore Laravel packages today?