runasroot / module-seeder
runasroot/module-seeder
Laravel-style database seeding for Magento 2
Magento 2 Database Seeder
Laravel-style database seeding for Magento 2 / Mage-OS. Define simple PHP / JSON / YAML files (or use the built-in Faker generators), run bin/magento db:seed, populate your dev environment with realistic products, categories, customers, orders, CMS content, reviews, cart rules, wishlists, and newsletter subscribers.
[image: db:seed --generate=order:10 --fresh]
Installation
composer require runasroot/module-seeder --dev
bin/magento module:enable RunAsRoot_Seeder
bin/magento setup:upgrade
Quick Start
- Scaffold a seeder:
bin/magento db:seed:make - Run
bin/magento db:seed
Usage
# Run all seeders
bin/magento db:seed
# Run only specific types
bin/magento db:seed --only=customer,order
# Skip specific types
bin/magento db:seed --exclude=cms
# Wipe relevant data and re-seed
bin/magento db:seed --fresh
# Stop on first error
bin/magento db:seed --stop-on-error
# Combine flags
bin/magento db:seed --fresh --only=customer,product
# Show current DB counts of seeded entities
bin/magento db:seed:status
Scaffolding
db:seed:make creates one or more seeder files for you — no need to memorize the format.
# Interactive — multi-select entity types, per-type counts, one file per type.
# Labels show cascade hints, e.g. "order — cascades: customer, product, category".
bin/magento db:seed:make
# Flag-driven (CI / scripts) — single type per invocation.
bin/magento db:seed:make --type=order --count=100 --format=php
# Overwrite an existing file
bin/magento db:seed:make --type=order --count=100 --force
Interactive mode pairs a multi-select (use space to toggle, enter to confirm) with a
per-type count prompt — picking order, customer writes both OrderSeeder.<ext>
and CustomerSeeder.<ext>. Shared prompts (locale, seed, format) apply to every
file written in the same run. If any target already exists you'll get a per-file
overwrite confirm; declined files are skipped and the rest continue.
Flag-driven mode stays single-type (--type=X --count=N); multi-type via flags
is not supported yet.
Available flags:
| Flag | Default | Notes |
|---|---|---|
--type |
— | required non-interactive |
--count |
— | required non-interactive |
--format |
php |
php / json / yaml |
--name |
{Type}Seeder |
file name without extension |
--locale |
en_US |
Faker locale |
--seed |
random | Faker seed for deterministic output |
--force |
false |
overwrite existing file |
Seeder Formats
Seeder files live in dev/seeders/ and must end in Seeder.<ext>. Three formats are supported:
| Format | Extension | Use when |
|---|---|---|
| PHP | .php |
Array or class with loops / Faker / conditionals |
| JSON | .json |
Machine-generated fixtures or cross-language tools |
| YAML | .yaml / .yml |
Human-readable fixtures |
All three share the same payload shape: type, data (or count), and optional order/locale/seed.
Array-Based (PHP)
Create a PHP file ending in Seeder.php that returns an array with type and data:
<?php
// dev/seeders/CustomerSeeder.php
return [
'type' => 'customer',
'data' => [
['email' => '[email protected]', 'firstname' => 'John', 'lastname' => 'Doe', 'password' => 'Test1234!'],
['email' => '[email protected]', 'firstname' => 'Jane', 'lastname' => 'Doe', 'password' => 'Test1234!'],
],
];
JSON
// dev/seeders/CustomerSeeder.json
{
"type": "customer",
"data": [
{"email": "[email protected]", "firstname": "John", "lastname": "Doe", "password": "Test1234!"},
{"email": "[email protected]", "firstname": "Jane", "lastname": "Doe", "password": "Test1234!"}
]
}
YAML
# dev/seeders/CustomerSeeder.yaml
type: customer
data:
- email: [email protected]
firstname: John
lastname: Doe
password: Test1234!
- email: [email protected]
firstname: Jane
lastname: Doe
password: Test1234!
Invalid JSON/YAML files are logged to var/log/ and skipped; the rest of the run continues.
Class-Based (fluent, recommended)
For complex scenarios, extend RunAsRoot\Seeder\Seeder and use the fluent builder:
<?php
// dev/seeders/MassOrderSeeder.php
use RunAsRoot\Seeder\Seeder;
class MassOrderSeeder extends Seeder
{
public function getType(): string { return 'order'; }
public function getOrder(): int { return 40; }
public function run(): void
{
$this->orders()
->count(50)
->with(['items' => [['sku' => 'TSHIRT-001', 'qty' => 2]]])
->create();
}
}
Available builder entry points: customers(), products(), orders(), categories(), cms(), plus seed('custom_type') for types registered via di.xml.
Builder methods:
| Method | Purpose |
|---|---|
->count(int $n) |
How many to create |
->with(array $data) |
Static overrides merged into each iteration (shallow replace) |
->using(callable $fn) |
Per-iteration callback: fn(int $i, Faker\Generator $faker): array |
->subtype(string $s) |
Force subtype (e.g. 'bundle' for products, 'complete' for orders) |
->create() |
Executes and returns int[] of created ids |
Precedence (most specific wins): using() > with() > generator Faker defaults.
If your subclass needs its own dependencies, override the constructor and call parent::__construct(...):
public function __construct(
EntityHandlerPool $handlers,
DataGeneratorPool $generators,
FakerFactory $fakerFactory,
GeneratedDataRegistry $registry,
private readonly MyService $svc,
) {
parent::__construct($handlers, $generators, $fakerFactory, $registry);
}
Class-Based (low-level)
If you need full control, implement SeederInterface directly and inject EntityHandlerPool:
class CustomSeeder implements SeederInterface
{
public function __construct(private readonly EntityHandlerPool $handlerPool) {}
public function getType(): string { return 'order'; }
public function getOrder(): int { return 40; }
public function run(): void
{
$this->handlerPool->get('order')->create([...]);
}
}
Supported Entity Types
| Type | What it creates |
|---|---|
customer |
Customer accounts |
category |
Category tree nodes |
product |
Products (all five Magento types) |
order |
Orders via quote-to-order flow |
cms |
CMS pages and blocks |
cart_rule |
Shopping-cart price rules with a specific coupon each |
wishlist |
Wishlists with 1–5 product items per seeded customer |
newsletter_subscriber |
Newsletter subscribers (50/50 linked customers vs guests) |
Default Seeding Order
- Categories (10)
- Products (20)
- Customers (30)
- Orders (40)
- CMS / Cart Rules (50)
- Wishlists (60)
- Newsletter Subscribers (70)
Override with 'order' => 5 in array seeders or getOrder(): int in class seeders.
The --fresh Flag
When using --fresh, the module cleans existing data before seeding:
- Customers: Deletes all non-admin customers
- Products: Deletes all products
- Categories: Deletes all categories except root (ID 1) and default (ID 2)
- Orders: Deletes all orders (FK cascades handle invoices, shipments, etc.)
- CMS: Only deletes pages/blocks with the
seed-identifier prefix - Cart Rules: Only deletes rules whose name starts with
Seed Rule —(attached coupons cascade) - Wishlists: Only deletes wishlists whose owner's email matches
%@example.%(items cascade via FK) - Newsletter Subscribers: Only deletes rows whose email matches
%@example.%
Clean runs in reverse dependency order (later-ordered types first).
Extending
Add custom entity handlers via di.xml:
<type name="RunAsRoot\Seeder\Service\EntityHandlerPool">
<arguments>
<argument name="handlers" xsi:type="array">
<item name="custom_entity" xsi:type="object">Vendor\Module\Seeder\CustomEntityHandler</item>
</argument>
</arguments>
</type>
Your handler must implement RunAsRoot\Seeder\Api\EntityHandlerInterface.
Data Generation with Faker
Generate realistic fake data at scale using the --generate flag. No seeder files needed.
Basic Usage
# Generate 1000 orders
bin/magento db:seed --generate=order:1000
# Generate multiple entity types
bin/magento db:seed --generate=order:1000,customer:500
# Use a specific locale
bin/magento db:seed --generate=customer:100 --locale=de_DE
# Deterministic output (same seed = same data)
bin/magento db:seed --generate=product:50 --seed=42
# Combine with --fresh to wipe and regenerate
bin/magento db:seed --generate=order:500 --fresh
Progress bar
When a resolved count for any type is 10 or more, the command renders a per-type
Symfony Console progress bar so long runs show live progress. Smaller counts keep the
compact Generated N type(s)... done output. Nothing to configure.
Smart Dependency Resolution
You only need to request the entities you want. Dependencies are auto-generated with sensible ratios.
For example, --generate=order:1000 will also generate the required customers, products, and categories automatically.
| Requested | Auto-generates |
|---|---|
order:1000 |
customer:200 (1:5 ratio), product:50 (1:20 ratio), category:10 (1:5 of products) |
product:100 |
category:20 (1:5 ratio) |
wishlist:50 |
customer:50 (1:1 ratio); products reused from whatever's in the DB/registry |
customer:500 |
Nothing (no dependencies) |
category:50 |
Nothing (no dependencies) |
cms:20 |
Nothing (no dependencies) |
cart_rule:20 |
Nothing (no dependencies) |
newsletter_subscriber:100 |
Nothing — links to customers already in the registry, falls back to guest emails |
If you explicitly request a dependency type, your count takes precedence over the auto-calculated one.
Count-Based Seeder Files
Instead of listing individual data entries, use the count key to generate Faker data from a seeder file:
<?php
// dev/seeders/GenerateOrderSeeder.php
return [
'type' => 'order',
'count' => 100,
'locale' => 'en_US',
];
This triggers the Faker generation pipeline (with dependency resolution) instead of the standard array-based data flow.
Commerce-quality fake data
Faker's default words() / sentence() helpers produce lorem-ipsum —
fine for description fields, bad for product names. This module
registers a CommerceProvider on every Faker\Generator it hands out,
mirroring the commerce module from @faker-js/faker (MIT).
Methods available on $faker in any custom seeder / data generator:
| Method | Example |
|---|---|
$faker->productName() |
Handcrafted Rubber Pizza |
$faker->productAdjective() |
Handcrafted |
$faker->productMaterial() |
Rubber |
$faker->product() |
Pizza |
$faker->productDepartment() |
Electronics |
Used internally by ProductDataGenerator (product names) and
CategoryDataGenerator (category names). Locale is en_US only in v1;
other locales silently fall back to English wordlists. See
src/Faker/Provider/Data/Commerce/README.md for refresh + locale-extension
instructions.
Product Reviews
Every seeded product automatically gets 0–10 reviews with Faker-generated nicknames, titles, details, and a 1–5 star rating. Reviews are created against the default store (id 1) with status Approved so they render on the frontend immediately.
No CLI flag required — reviews are part of the product seed payload. If you want to disable reviews temporarily, set the reviews count range in src/DataGenerator/ProductDataGenerator.php (generateReviews() helper).
Product Types
The seeder supports all five standard Magento product types. Plain --generate=product:N produces a weighted mix; dotted subtypes force a specific type.
CLI
bin/magento db:seed --generate=product:100
bin/magento db:seed --generate=product.configurable:20,product.bundle:10
bin/magento db:seed --generate=product:100,product.bundle:20 # mix + force
Default weights (for plain product:N)
| Subtype | Weight |
|---|---|
| simple | 70% |
| configurable | 10% |
| bundle | 10% |
| grouped | 5% |
| downloadable | 5% |
Change weights in src/DataGenerator/ProductDataGenerator.php — SUBTYPE_WEIGHTS constant.
Per-type behavior
- Simple: as before.
- Configurable: auto-creates 6 hidden simple children spanning 3 color options × 2 size options. Requires
colorandsizeattributes with option values on the target install — if either is missing or empty, configurable generation fails fast with a clear error. The module does not create attributes. - Bundle: creates a dynamic-price bundle with up to 3 options (select / radio / checkbox), each linking 2–3 existing simples. Falls back from registry → SEED-% products in DB → warns and skips if the simple pool is empty.
- Grouped: links up to 5 existing simples via
catalog_product_link(link type 3). Same fallback chain as bundle. - Downloadable: attaches 1–2 file-type links backed by Faker-generated
.txtsamples underpub/media/downloadable/files/andpub/media/downloadable/files_sample/.
Category distribution
Products are assigned to the category with the fewest products so far (ties go to the earliest category). As long as the run produces at least as many products as categories, every category ends up with at least one product.
Custom Data Generators
Add your own data generators via di.xml:
<type name="RunAsRoot\Seeder\Service\DataGeneratorPool">
<arguments>
<argument name="generators" xsi:type="array">
<item name="custom_entity" xsi:type="object">Vendor\Module\Seeder\CustomEntityDataGenerator</item>
</argument>
</arguments>
</type>
Your generator must implement RunAsRoot\Seeder\Api\DataGeneratorInterface.
Order States
Orders are generated across the real Magento lifecycle states. Plain --generate=order:N produces a weighted mix; dotted subtypes force a specific state.
CLI
bin/magento db:seed --generate=order:100
bin/magento db:seed --generate=order.complete:50,order.canceled:10
bin/magento db:seed --generate=order:100,order.holded:5 # mix + force
Default state weights
| State | Weight |
|---|---|
| new | 15% |
| processing | 25% |
| complete | 40% |
| canceled | 10% |
| holded | 5% |
| closed | 5% |
Change weights in src/DataGenerator/OrderDataGenerator.php — STATE_WEIGHTS constant. States pending_payment and payment_review are intentionally skipped; they require payment-gateway plumbing and add little dev-data value.
Per-state behavior
- new: default state after
CartManagementInterface::placeOrder. No additional action. - processing: order is invoiced offline (
InvoiceService::prepareInvoice+registerwithCAPTURE_OFFLINE). - complete: invoice (as above), then full shipment via
ShipmentFactory::create($order, $itemQtyMap)+register. - canceled:
$order->cancel(). - holded:
$order->hold(). - closed: invoice, then offline refund via
CreditmemoFactory::createByOrder+CreditmemoManagementInterface::refund($memo, true).
After each invoice-based transition, the order state is explicitly set and saved a second time because some Magento observer chains reset the state during the transaction save. Without this, invoiced orders would remain stuck at new.
Order items
Seeded orders only use simple products as cart items — bundles, configurables, grouped, and downloadables require per-cart option selections that would complicate the seeder. OrderDataGenerator declares product.simple as its product dependency, so the dependency resolver always produces the right type.
Cart Rules
Plain --generate=cart_rule:N creates N sales rules, each with one attached manual-code coupon.
CLI
bin/magento db:seed --generate=cart_rule:20
Action mix
| simple_action | Weight | Discount amount | Coupon prefix |
|---|---|---|---|
by_percent |
60% | 5–30 (percent off) | SAVE |
by_fixed |
30% | 5–50 (fixed currency off) | DEAL |
free_shipping |
10% | n/a (sets FREE_SHIPPING_ITEM) | PROMO |
Change weights in src/DataGenerator/CartRuleDataGenerator.php — ACTION_WEIGHTS constant.
Rule shape
- Active for all websites (id 1) and all four default customer groups (NOT LOGGED IN + General + Wholesale + Retailer).
- Empty conditions tree — applies to every cart.
to_date= today + 1 year. Nofrom_daterestriction.- Rule name:
Seed Rule — <two Faker words>so--freshcan scope cleanup. - Coupon code:
<PREFIX><amount>-<6 uppercase alnum>, e.g.SAVE12-AB1234. Collision retry with a random suffix, up to 3 attempts.
Wishlists
Plain --generate=wishlist:N creates one wishlist per seeded customer, each with 1–5 randomly-picked products.
CLI
bin/magento db:seed --generate=wishlist:50
Requires at least 1 customer and 1 product in the registry (auto-generated per the dependency resolver — see the table above).
Items
qty= 1,shared= 0.- Items are inserted directly into
wishlist_item, bypassingWishlist::addNewItem's stock guard. This keeps the seeder resilient to freshly-seeded products whose stock index hasn't caught up yet. Tradeoff: only simple products are exercised today — configurable / bundle wishlist items would need the fulladdNewItemoption-serialization path.
Cleanup
--fresh scopes wishlist cleanup to customers whose email matches %@example.% (Faker's safeEmail() domain). wishlist_item rows cascade via FK.
Newsletter Subscribers
Plain --generate=newsletter_subscriber:N creates N subscriber rows with a roughly 50/50 mix of customer-linked and guest (unlinked) entries.
CLI
bin/magento db:seed --generate=newsletter_subscriber:100
Behavior
- If any customers are in the registry, ~half the subscribers reuse their emails +
customer_id; the other half get guest Faker emails withcustomer_id=0. - Each customer is linked at most once per run — dedup is derived from the subscribers already in the registry so state never leaks between runs.
- Status is always
Subscriber::STATUS_SUBSCRIBED. Store id 1.status_changed_atset to current timestamp.
Cleanup
--fresh scopes cleanup to subscriber_email LIKE '%@example.%'. If your install has real users with @example.* addresses, they will also match — inline comments in the handler flag this. Prefix your real users off @example.* if that's a concern.
Performance
Iterations are batched into database transactions of 50 entries each. For large runs (e.g. --generate=product:5000), this cuts per-iteration commit overhead roughly in half. A failing iteration rolls back only its batch's pending work and continues with the next batch.
License
MIT
Changelog
All notable changes to runasroot/module-seeder are documented in this file.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
[1.4.0] - 2026-04-22
Added
CommerceProvider— a newFaker\Provider\Basesubclass registered on everyFaker\GeneratorthatFakerFactoryhands out. Exposes five methods mirroring thecommercemodule from @faker-js/faker (MIT):$faker->productName(),->productAdjective(),->productMaterial(),->product(),->productDepartment(). Eliminates lorem-ipsum product names in seeded catalogs —Dolor Sit AmetbecomesHandcrafted Rubber Pizza.CommerceLocaleInterface+CommerceProviderFactory— locale-aware wiring with silent English fallback.en_USships with 27 adjectives, 15 materials, 24 products, and 22 departments ported verbatim from @faker-js/faker'sencommerce locale at upstream commit9e2c0e391b436f56ff54ad89d02efa9982406389. Unknown locales (incl.de_DEin v1) fall back to the English wordlists without warning — mirrors how Faker itself handles unmapped locales.src/Faker/Provider/Data/Commerce/README.md— refresh + locale-extension guide. Explains how to re-sync wordlists from FakerJS (manual, ~5 min) and how to add new locale data classes.NOTICEat repo root — MIT attribution for the ported wordlists, pinned to the source commit.- README section documenting the new
$faker->product*()methods with a one-line example per method and a note on locale scope.
Changed
ProductDataGenerator::generate()now uses$faker->productName()in place ofucwords($faker->words(2-4, true)). Product names are now three-word commerce-style strings drawn from curated upstream wordlists. Name-field assertions in user seeder tests that relied on the lorem-word shape will need updating (shape assertions like "non-empty string" keep working unchanged).CategoryDataGenerator::generate()now uses$faker->productDepartment()in place of the hardcoded 20-entryCOMMERCE_CATEGORIESconst + random-lorem-word suffix. Names likeElectronics laborebecome cleanBooks/Jewelery/Automotive. TheCOMMERCE_CATEGORIESprivate const was removed.
Fixed
- n/a — this release is purely additive in user-facing behavior.
Installation
composer require runasroot/module-seeder:^1.4
bin/magento setup:upgrade
Fully backward compatible with 1.3.x — no breaking public API changes. FakerFactory::__construct() gained an optional ?CommerceProviderFactory argument; the factory self-constructs a default when none is passed, so any caller doing new FakerFactory() keeps working unchanged. No new composer requires.
Contributors
- @DavidLambauer — entire release
[1.3.0] - 2026-04-21
Added
- New
bin/magento db:seed:makecommand — interactive scaffolder for seeder files. Eliminates the empty-dev/seeders/dead-end: new users can now go from zero to a working seeder without reading the README. Supports both interactive mode (TTY) and flag-driven mode (--type,--count,--format,--name,--locale,--seed,--force) for CI/scripts. - Multi-select entity types in the interactive flow: pick any combination from the eight supported types in one prompt, get one scaffolded file per selected type. Each type gets its own
countprompt; locale, seed, and format are shared. - Cascade hints in the type picker — labels show dependency relationships so users understand what seeding one type implies (e.g.
order — cascades: customer, product, category). Purely informational;DependencyResolverstill handles resolution at seed time. - Progress rendering for count-based file seeders. Running
db:seedagainst a scaffoldedcount: 100file used to run silent for minutes; now renders a Laravel Prompts progress bar via the newProgressReporterservice. Threshold matches the old--generatebehavior (count >= 10). - Spinner for custom (non-count)
SeederInterfaceimplementations in file-based seed runs. TTY-gated so CI pipelines and log files stay clean. SeederFileBuilderservice — pure, stateless, unit-testable without Magento bootstrap. Emits PHP / JSON / YAML shapes that roundtrip through the existingArraySeederAdapterloader. Hardened against quote escaping and code injection in$type/$localeviavar_export().Test/Integration/Console/SeedMakeRoundtripTest— integration coverage of the full scaffold → seed pipeline against a live Mage-OS install. Uses@magentoDbIsolation enabled+ delta-based assertions.- README section documenting the scaffolder with interactive + flag-driven examples and a flag reference table.
Changed
db:seedon an empty/missingdev/seeders/now printsRun bin/magento db:seed:make to scaffold one.alongside the existingNo seeders foundmessage. Exit code unchanged.SeederRunner::run()now accepts an optional?callable $onProgresssecond argument. Fully BC — existing callers passing only$configkeep working.ArraySeederAdapternow exposessetProgressCallback(?callable)andhasCount()so file-based count seeders can render progress identical to--generate=type:N.--generateprogress rendering migrated from SymfonyProgressBarto Laravel PromptsProgress. Visual-only change, same$total < 10threshold.
Fixed
- Progress cursor restoration —
SeedCommand::executeGenerate()now wrapsGenerateRunner::run()intry { ... } finally { $progressReporter->finish() }so the terminal cursor is always restored, even if generation throws. - Scaffolder rejects
--namevalues that don't end inSeeder(matches the globSeederDiscoveryrequires; otherwise the scaffolded file would be silently ignored atdb:seedtime). - Scaffolder's interactive flow no longer re-prompts for
--locale=en_US/--format=phpwhen the user explicitly passed those defaults on the CLI.
Installation
composer require runasroot/module-seeder:^1.3
bin/magento setup:upgrade
Fully backward compatible with 1.2.x — no breaking public API changes. Adds two composer requires: laravel/prompts: ^0.3 (runtime, for the scaffolder + progress UI) and mockery/mockery: ^1.6 (dev-only, for Prompt::fake() in tests).
Contributors
- @DavidLambauer — entire release
[1.2.1] - 2026-04-20
Fixed
db:seed --freshno longer crashes with "Delete operation is forbidden for current area" on installs that carry sample data (or any non-seeder products/categories).SeedCommandnow registersisSecureArea = truealongside the existingadminhtmlarea code, so category/product cleanup can pass Magento's delete guards from CLI. Registration is skipped when the flag is already set by an outer caller to avoid clobbering.
1.2.0 - 2026-04-20
Added
- Three new entity types:
cart_rule,wishlist,newsletter_subscriber— each with aDataGenerator+EntityHandlerpair. Reachable via--generate=cart_rule:N,--generate=wishlist:N,--generate=newsletter_subscriber:N, or the fluentseed('<type>')builder entry. CartRuleDataGenerator— weighted action mix ofby_percent(60%) /by_fixed(30%) /free_shipping(10%). Each rule gets one attached manual coupon with code format<PREFIX><amount>-<6 uppercase alnum>, active for all websites + all four default customer groups, expires in 1 year, applies to all carts (empty condition tree).WishlistDataGenerator— one wishlist per seeded customer with 1–5 random products. Declarescustomer:Nas dependency so--generate=wishlist:50auto-seeds 50 customers. Handler inserts intowishlist_itemdirectly to sidestep stock-index race conditions on freshly seeded products.NewsletterSubscriberDataGenerator— 50/50 mix of customer-linked subscribers (email reused from registry) and guest emails. Dedup of linked customers derived from registry state so state never leaks between runs.CustomerDataGeneratornow emits 1–3 addresses per customer (first remains default billing/shipping, extras are non-default).Test/Integration/NewEntityTypesSmokeTest— integration smoke coverage for all three new types against a real Mage-OS install (rules + coupons created, subscribers split linked/guest, wishlists + items + qty verified).- 23 new unit tests covering handler branches:
CartRuleHandlerTest(retry loop on coupon collision, free_shipping branch, cleanup filter),WishlistHandlerTest(direct-insert bind columns, store_id fallback, customer-scoped cleanup),NewsletterSubscriberHandlerTest(load-or-merge, default coalesce, cleanup). - README sections documenting Cart Rules, Wishlists, and Newsletter Subscribers (CLI, action/behavior mix, cleanup scope).
Changed
src/etc/di.xml: allEntityHandlerPoolandDataGeneratorPoolitems now wire through\Proxy.bin/magento setup:installwas eagerly instantiating EAV-touching handlers (Customer, Product) before the schema existed, crashing withTableNotFoundException: eav_entity_type. Proxies defer construction to first use — idiomatic for handler pools behind console commands.
Fixed
ProductHandlerresolves the image import directory viaDirectoryList::getPath(DirectoryList::MEDIA)instead of hard-codedgetRoot() . '/pub/media/...'. Sandboxed installs that remapMEDIA(split-pub deployments, integration test harness) no longer fail PathValidator checks.
Installation
composer require runasroot/module-seeder:^1.2
bin/magento setup:upgrade
Fully backward compatible with 1.1.x — no public API changes. Adds three new composer requires (magento/module-sales-rule, magento/module-wishlist, magento/module-newsletter) which every modern Magento 2 / Mage-OS install already ships with.
Contributors
- @DavidLambauer — entire release
1.1.0 - 2026-04-20
Added
- Abstract
RunAsRoot\Seeder\Seederbase class so class-based seeders skip theEntityHandlerPoolboilerplate and extend a typed fluent base. - Fluent
RunAsRoot\Seeder\SeedBuilderAPI:$this->orders()->count(50)->with([...])->using($fn)->subtype('bundle')->create(). Per-iteration callbacks receive(int $i, Faker\Generator $faker). examples/FluentOrderSeeder.phpdemonstrating the new style.
Changed
SeedBuilder::create()writes created entity data (including theidreturned by the handler) intoGeneratedDataRegistryunder the base type, matchingGenerateRunnersemantics. This means later builders within the samerun()can reference ids through generators.
1.0.0 - 2026-04-19
First stable release. Establishes the public API baseline for
RunAsRoot\Seeder\Api\EntityHandlerInterface and the db:seed /
db:seed:status CLI surface.
Added
CLI
bin/magento db:seedfor array-based seeder files indev/seeders/—--only,--exclude,--fresh,--stop-on-errorbin/magento db:seed --generate=type:N[,type2:N2,...]for Faker-powered mass data generation with smart dependency resolution (requestingorder:1000auto-generates customers / products / categories at sensible ratios)--localeand--seedflags for deterministic / localized generationbin/magento db:seed:status— prints DB counts of all seeded entity types (products, categories, customers, orders, CMS pages / blocks, reviews)
Entity support
- Five core handlers:
category,product,customer,order,cms - Five product type builders:
simple,configurable(3×2 color / size variants),bundle(3 options, dynamic pricing),grouped(links up to 5 simple products),downloadable(Faker-generated sample / link files viaaddImageToMediaGallery+ContentInterface) - Dotted subtype syntax
product.configurable:Nalongside weighted splitproduct:N - Order state transitions:
new,holded,canceled,processing(invoice offline),complete(invoice + shipment),closed(invoice + offline credit memo) - Automatic product reviews: each seeded product gets 0–10 Faker-generated reviews (nickname, title, detail, 1–5 star rating) attached as Approved on store 1 so they render immediately on the frontend. Rating application is resilient — a single bad rating no longer skips the rest
Infrastructure
DependencyResolver— auto-generates missing dependency types at ratios (order:1000→customer:200,product:50,category:10)GeneratedDataRegistry— carries saved entity ids across generators so downstream types can reference upstream entities (products → categories, orders → customers + products)- Even category distribution for products via least-used-first algorithm
ImageDownloader— picsum.photos integration, images attached viaaddImageToMediaGallerywithmove=trueso they land inpub/media/catalog/product/instead of orphaned inimport/ArraySeederAdapter— bridges simple array config (['type' => ..., 'data' => [...]]) to the generator pipelineReviewCreatorservice wrapping Magento's Review + Rating API with swallowed per-review errors.freshprotection — root + default categories never get cleaned; CMS usesseed-identifier prefix for conservative cleanup;SEED-%SKUs scope product + review cleanup- 202 unit tests with Magento-free bootstrap stubs in
tests/bootstrap.php
Repository
- MIT
LICENSE - GitHub Actions CI workflow
- Pull request template
Changed
- BREAKING (#2):
EntityHandlerInterface::create(array $data): voidis now: intand returns the saved entity primary key. Consumer projects with customEntityHandlerInterfaceimplementations registered viadi.xmlmust update their return type — PHP will surface this at class-load with a clear "declaration must be compatible" error.
Fixed
- BREAKING behaviour change (#2):
GenerateRunnernow writes the handler-returned entity id intoGeneratedDataRegistrybefore passing it to downstream generators. Previously every seeded product landed in root "Default Category" (id 2) regardless of seeded categories, becauseProductDataGenerator's$category['id'] ?? 2always fell through to the fallback. Consumers relying on the bugged behaviour will see products correctly distributed across seeded categories after upgrading. CustomerDataGeneratorsanitizes FakerphoneNumber()to match Magento's regex (0-9 + - ( ) space). Faker's US format includes.,x,ext.which previously caused 100% customer save failures on some seeds and cascaded into order failures.ProductHandlersetssetStockData()+setWebsiteIds()before save and forcescataloginventory_stockreindex soQuote::addProductaccepts products when the indexer is in Schedule mode.OrderHandlersets the current store viaStoreManagerInterfaceand assignsstore_idto the quote before placing; previouslycreateEmptyCart()ran in admin context (store_id=0) andQuote::addProductrejected products as "not available".GenerateRunnertracks a failed count per type and reportssuccess=falseon any iteration throwing.SeedCommandnow exits non-zero with the error message. Previously silent: "Generated 0 order(s)... done" exited 0 even when every iteration threw.- Order state now persists after invoice / shipment / credit-memo transitions — brute-force reload + save after the
InvoiceService/ShipmentFactorypipeline, because the internal state resolver sometimes left orders stuck onnew. OrderDataGeneratoronly picks simple products as order items, and forces a simple-product dependency so orders always have usable items even in mixed-type runs.
Installation
composer require runasroot/module-seeder:^1.0
bin/magento module:enable RunAsRoot_Seeder
bin/magento setup:upgrade
bin/magento setup:di:compile
Contributors
- @DavidLambauer — entire release
| Version | Stability | QA Status | Compatibility | Released |
|---|---|---|---|---|
| 1.4.0 | stable | Fail | Magento 2.4.7-2.4.8 Details | 2026-04-22 04:38:17 |
| 1.3.0 | stable | Not tested | Not yet tested Details | 2026-04-21 20:04:17 |
| 1.2.1 | stable | Not tested | Not yet tested Details | 2026-04-20 12:34:11 |
| 1.2.0 | stable | Not tested | Not yet tested Details | 2026-04-20 11:44:55 |
| 1.1.0 | stable | Not tested | Not yet tested Details | 2026-04-20 09:47:48 |
| 1.0.0 | stable | Not tested | Not yet tested Details | 2026-04-19 09:56:25 |
Requires 19
| Package | Constraint |
|---|---|
| php | ^8.1 |
| magento/framework | * |
| magento/module-bundle | * |
| magento/module-catalog | * |
| magento/module-catalog-inventory | * |
| magento/module-checkout | * |
| magento/module-cms | * |
| magento/module-configurable-product | * |
| magento/module-customer | * |
| magento/module-downloadable | * |
| magento/module-grouped-product | * |
| magento/module-newsletter | * |
| magento/module-quote | * |
| magento/module-sales | * |
| magento/module-sales-rule | * |
| magento/module-wishlist | * |
| fakerphp/faker | ^1.23 |
| laravel/prompts | ^0.3 |
| symfony/yaml | ^6.0 || ^7.0 |
Requires-dev 7
| Package | Constraint |
|---|---|
| phpunit/phpunit | ^10.0 |
| symfony/console | ^6.0 || ^7.0 |
| phpstan/phpstan | ^1.11 |
| magento/magento-coding-standard | * |
| squizlabs/php_codesniffer | ^3.10 |
| psr/log | ^3.0 |
| mockery/mockery | ^1.6 |
Compatibility
Each Magento release line is installed on its supported PHP versions, then the module is built (DI compilation + static-content deploy) and its unit and integration suites are run. The matrix shows the lines and PHP versions the module is confirmed to install and run on. Code-quality results further down (phpstan, phpcs, …) are reported separately and never affect compatibility.
Code Quality
Advisory checks against the module's source. Static analysis runs once across the whole module; PHPStan re-runs per Magento + PHP version because resolvable symbols differ between releases. These NEVER affect the Compatibility badge — a phpcs finding can't make a module incompatible.
Static analysis
Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.
| Tool | Status | Findings | Summary |
|---|---|---|---|
| PHPCS | Fail | 851 | 300 errors, 551 warnings (ruleset: Magento2) — 93 auto-fixable with phpcbf |
| PHPMD | Warning | 43 | 43 rule violations (MissingImport:29, CyclomaticComplexity:4, NPathComplexity:3, ExcessiveMethodLength:2, EmptyCatchBlock:2) |
| Cpd | Warning | 9 | 9 duplicated chunks spanning 188 total lines (min-lines=5, min-tokens=70) |
| Composer validate | Info | 15 | valid; 15 advisory notes (composer validate --strict) |
PHPStan
Type-checks the module's PHP against a real Magento install at the configured gate level. Re-runs per Magento and PHP version because resolvable symbols differ between releases. Cell → details modal.
Tests
Unit and integration suites, run for each applicable Magento and PHP version. A test failure speaks to the module's behaviour, not its compatibility with a Magento line, so it is reported here separately and never reddens the compatibility matrix.
Unit tests
Security
Security checks run directly against the module: an audit of its declared dependencies for known vulnerabilities (composer audit) and a scan of its source for malware and web-shell signatures. Each runs once. A malware detection fails the version outright.
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.