runasroot / module-seeder

runasroot/module-seeder

Laravel-style database seeding for Magento 2

magento2-module Compatibility: 2.4.7-2.4.8 Code Quality: Fail Tests: Fail Security: Pass MIT

Magento 2 Database Seeder

CI
License: MIT

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

  1. Scaffold a seeder: bin/magento db:seed:make
  2. 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

  1. Categories (10)
  2. Products (20)
  3. Customers (30)
  4. Orders (40)
  5. CMS / Cart Rules (50)
  6. Wishlists (60)
  7. 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.phpSUBTYPE_WEIGHTS constant.

Per-type behavior

  • Simple: as before.
  • Configurable: auto-creates 6 hidden simple children spanning 3 color options × 2 size options. Requires color and size attributes 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 .txt samples under pub/media/downloadable/files/ and pub/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.phpSTATE_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 + register with CAPTURE_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.phpACTION_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. No from_date restriction.
  • Rule name: Seed Rule — <two Faker words> so --fresh can 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, bypassing Wishlist::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 full addNewItem option-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 with customer_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_at set 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 new Faker\Provider\Base subclass registered on every Faker\Generator that FakerFactory hands out. Exposes five methods mirroring the commerce module from @faker-js/faker (MIT): $faker->productName(), ->productAdjective(), ->productMaterial(), ->product(), ->productDepartment(). Eliminates lorem-ipsum product names in seeded catalogs — Dolor Sit Amet becomes Handcrafted Rubber Pizza.
  • CommerceLocaleInterface + CommerceProviderFactory — locale-aware wiring with silent English fallback. en_US ships with 27 adjectives, 15 materials, 24 products, and 22 departments ported verbatim from @faker-js/faker's en commerce locale at upstream commit 9e2c0e391b436f56ff54ad89d02efa9982406389. Unknown locales (incl. de_DE in 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.
  • NOTICE at 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 of ucwords($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-entry COMMERCE_CATEGORIES const + random-lorem-word suffix. Names like Electronics labore become clean Books / Jewelery / Automotive. The COMMERCE_CATEGORIES private 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:make command — 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 count prompt; 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; DependencyResolver still handles resolution at seed time.
  • Progress rendering for count-based file seeders. Running db:seed against a scaffolded count: 100 file used to run silent for minutes; now renders a Laravel Prompts progress bar via the new ProgressReporter service. Threshold matches the old --generate behavior (count >= 10).
  • Spinner for custom (non-count) SeederInterface implementations in file-based seed runs. TTY-gated so CI pipelines and log files stay clean.
  • SeederFileBuilder service — pure, stateless, unit-testable without Magento bootstrap. Emits PHP / JSON / YAML shapes that roundtrip through the existing ArraySeederAdapter loader. Hardened against quote escaping and code injection in $type / $locale via var_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:seed on an empty/missing dev/seeders/ now prints Run bin/magento db:seed:make to scaffold one. alongside the existing No seeders found message. Exit code unchanged.
  • SeederRunner::run() now accepts an optional ?callable $onProgress second argument. Fully BC — existing callers passing only $config keep working.
  • ArraySeederAdapter now exposes setProgressCallback(?callable) and hasCount() so file-based count seeders can render progress identical to --generate=type:N.
  • --generate progress rendering migrated from Symfony ProgressBar to Laravel Prompts Progress. Visual-only change, same $total < 10 threshold.

Fixed

  • Progress cursor restoration — SeedCommand::executeGenerate() now wraps GenerateRunner::run() in try { ... } finally { $progressReporter->finish() } so the terminal cursor is always restored, even if generation throws.
  • Scaffolder rejects --name values that don't end in Seeder (matches the glob SeederDiscovery requires; otherwise the scaffolded file would be silently ignored at db:seed time).
  • Scaffolder's interactive flow no longer re-prompts for --locale=en_US / --format=php when 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 --fresh no longer crashes with "Delete operation is forbidden for current area" on installs that carry sample data (or any non-seeder products/categories). SeedCommand now registers isSecureArea = true alongside the existing adminhtml area 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 a DataGenerator + EntityHandler pair. Reachable via --generate=cart_rule:N, --generate=wishlist:N, --generate=newsletter_subscriber:N, or the fluent seed('<type>') builder entry.
  • CartRuleDataGenerator — weighted action mix of by_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. Declares customer:N as dependency so --generate=wishlist:50 auto-seeds 50 customers. Handler inserts into wishlist_item directly 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.
  • CustomerDataGenerator now 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: all EntityHandlerPool and DataGeneratorPool items now wire through \Proxy. bin/magento setup:install was eagerly instantiating EAV-touching handlers (Customer, Product) before the schema existed, crashing with TableNotFoundException: eav_entity_type. Proxies defer construction to first use — idiomatic for handler pools behind console commands.

Fixed

  • ProductHandler resolves the image import directory via DirectoryList::getPath(DirectoryList::MEDIA) instead of hard-coded getRoot() . '/pub/media/...'. Sandboxed installs that remap MEDIA (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\Seeder base class so class-based seeders skip the EntityHandlerPool boilerplate and extend a typed fluent base.
  • Fluent RunAsRoot\Seeder\SeedBuilder API: $this->orders()->count(50)->with([...])->using($fn)->subtype('bundle')->create(). Per-iteration callbacks receive (int $i, Faker\Generator $faker).
  • examples/FluentOrderSeeder.php demonstrating the new style.

Changed

  • SeedBuilder::create() writes created entity data (including the id returned by the handler) into GeneratedDataRegistry under the base type, matching GenerateRunner semantics. This means later builders within the same run() 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:seed for array-based seeder files in dev/seeders/--only, --exclude, --fresh, --stop-on-error
  • bin/magento db:seed --generate=type:N[,type2:N2,...] for Faker-powered mass data generation with smart dependency resolution (requesting order:1000 auto-generates customers / products / categories at sensible ratios)
  • --locale and --seed flags for deterministic / localized generation
  • bin/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 via addImageToMediaGallery + ContentInterface)
  • Dotted subtype syntax product.configurable:N alongside weighted split product: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:1000customer: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 via addImageToMediaGallery with move=true so they land in pub/media/catalog/product/ instead of orphaned in import/
  • ArraySeederAdapter — bridges simple array config (['type' => ..., 'data' => [...]]) to the generator pipeline
  • ReviewCreator service wrapping Magento's Review + Rating API with swallowed per-review errors
  • .fresh protection — root + default categories never get cleaned; CMS uses seed- 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): void is now : int and returns the saved entity primary key. Consumer projects with custom EntityHandlerInterface implementations registered via di.xml must update their return type — PHP will surface this at class-load with a clear "declaration must be compatible" error.

Fixed

  • BREAKING behaviour change (#2): GenerateRunner now writes the handler-returned entity id into GeneratedDataRegistry before passing it to downstream generators. Previously every seeded product landed in root "Default Category" (id 2) regardless of seeded categories, because ProductDataGenerator's $category['id'] ?? 2 always fell through to the fallback. Consumers relying on the bugged behaviour will see products correctly distributed across seeded categories after upgrading.
  • CustomerDataGenerator sanitizes Faker phoneNumber() 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.
  • ProductHandler sets setStockData() + setWebsiteIds() before save and forces cataloginventory_stock reindex so Quote::addProduct accepts products when the indexer is in Schedule mode.
  • OrderHandler sets the current store via StoreManagerInterface and assigns store_id to the quote before placing; previously createEmptyCart() ran in admin context (store_id=0) and Quote::addProduct rejected products as "not available".
  • GenerateRunner tracks a failed count per type and reports success=false on any iteration throwing. SeedCommand now 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 / ShipmentFactory pipeline, because the internal state resolver sometimes left orders stuck on new.
  • OrderDataGenerator only 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
Versions
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.

Compatibility matrix (Magento × PHP)
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 Pass Pass
2.4.8 Pass Pass
2.4.9 not tested not tested

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.

Static analysis results
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.

PHPStan results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 39 39
2.4.8 39 39
2.4.9 39 39

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

Unit tests results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 54 54
2.4.8 54 54
2.4.9 not tested Error

Integration tests

Integration tests results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 Error Error
2.4.8 Error Error
2.4.9 not tested Error

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.

Security results
Tool Status Findings Summary
Composer audit Pass 0
Malware scan Pass 0
License
MIT
Make it pay

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.