php-framework/docs/packages/php/seeder-system.md
Snider 632bca9111 feat(docs): restructure with PHP/Go framework sections
- Add auto-discovery sidebar with nested directory support
- Create packages index with search and grid layout
- Move framework docs to packages/php/
- Update nav: Guide | PHP | Go | Packages | Security

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 10:47:50 +00:00

14 KiB

Seeder System

The Seeder System provides automatic discovery, dependency resolution, and ordered execution of database seeders across modules. It supports both auto-discovery and manual registration with explicit priority and dependency declarations.

Overview

The Core seeder system offers:

  • Auto-discovery - Finds seeders in module directories automatically
  • Dependency ordering - Seeders run in dependency-resolved order
  • Priority control - Fine-grained control over execution order
  • Circular detection - Catches and reports circular dependencies
  • Filtering - Include/exclude seeders at runtime

Core Components

Class Purpose
SeederDiscovery Auto-discovers and orders seeders
SeederRegistry Manual seeder registration
CoreDatabaseSeeder Base seeder with discovery support
#[SeederPriority] Attribute for priority
#[SeederAfter] Attribute for dependencies
#[SeederBefore] Attribute for reverse dependencies
CircularDependencyException Thrown on circular deps

Discovery

Seeders are auto-discovered in Database/Seeders/ directories within configured module paths.

Discovery Pattern

{module_path}/*/Database/Seeders/*Seeder.php

For example, with module paths [app_path('Mod')]:

app/Mod/Blog/Database/Seeders/PostSeeder.php      // Discovered
app/Mod/Blog/Database/Seeders/CategorySeeder.php  // Discovered
app/Mod/Auth/Database/Seeders/UserSeeder.php      // Discovered

Using SeederDiscovery

use Core\Database\Seeders\SeederDiscovery;

$discovery = new SeederDiscovery([
    app_path('Core'),
    app_path('Mod'),
]);

// Get ordered seeders
$seeders = $discovery->discover();
// Returns: ['UserSeeder', 'CategorySeeder', 'PostSeeder', ...]

Priority System

Seeders declare priority using the #[SeederPriority] attribute or a public $priority property. Lower priority values run first.

Using the Attribute

use Core\Database\Seeders\Attributes\SeederPriority;
use Illuminate\Database\Seeder;

#[SeederPriority(10)]
class FeatureSeeder extends Seeder
{
    public function run(): void
    {
        // Runs early (priority 10)
    }
}

#[SeederPriority(90)]
class DemoDataSeeder extends Seeder
{
    public function run(): void
    {
        // Runs later (priority 90)
    }
}

Using a Property

class FeatureSeeder extends Seeder
{
    public int $priority = 10;

    public function run(): void
    {
        // Runs early
    }
}

Priority Guidelines

Range Use Case Examples
0-20 Foundation data Features, configuration, settings
20-40 Core data Packages, plans, workspaces
40-60 Default (50) General module seeders
60-80 Content data Pages, posts, products
80-100 Demo/test data Sample content, test users

Dependency Resolution

Dependencies ensure seeders run in the correct order regardless of priority. Dependencies take precedence over priority.

Using #[SeederAfter]

Declare that this seeder must run after specified seeders:

use Core\Database\Seeders\Attributes\SeederAfter;
use Mod\Feature\Database\Seeders\FeatureSeeder;

#[SeederAfter(FeatureSeeder::class)]
class PackageSeeder extends Seeder
{
    public function run(): void
    {
        // Runs after FeatureSeeder
    }
}

Multiple Dependencies

use Mod\Feature\Database\Seeders\FeatureSeeder;
use Mod\Tenant\Database\Seeders\TenantSeeder;

#[SeederAfter(FeatureSeeder::class, TenantSeeder::class)]
class WorkspaceSeeder extends Seeder
{
    public function run(): void
    {
        // Runs after both FeatureSeeder and TenantSeeder
    }
}

Using #[SeederBefore]

Declare that this seeder must run before specified seeders. This is the inverse relationship - you're saying other seeders depend on this one:

use Core\Database\Seeders\Attributes\SeederBefore;
use Mod\Package\Database\Seeders\PackageSeeder;

#[SeederBefore(PackageSeeder::class)]
class FeatureSeeder extends Seeder
{
    public function run(): void
    {
        // Runs before PackageSeeder
    }
}

Using Properties

As an alternative to attributes, use public properties:

class WorkspaceSeeder extends Seeder
{
    public array $after = [
        FeatureSeeder::class,
        PackageSeeder::class,
    ];

    public array $before = [
        DemoSeeder::class,
    ];

    public function run(): void
    {
        // ...
    }
}

Complex Ordering Examples

Example 1: Linear Chain

// Run order: Feature -> Package -> Workspace -> User

#[SeederPriority(10)]
class FeatureSeeder extends Seeder { }

#[SeederAfter(FeatureSeeder::class)]
class PackageSeeder extends Seeder { }

#[SeederAfter(PackageSeeder::class)]
class WorkspaceSeeder extends Seeder { }

#[SeederAfter(WorkspaceSeeder::class)]
class UserSeeder extends Seeder { }

Example 2: Diamond Dependency

//        Feature
//       /       \
//   Package    Plan
//       \       /
//       Workspace

#[SeederPriority(10)]
class FeatureSeeder extends Seeder { }

#[SeederAfter(FeatureSeeder::class)]
class PackageSeeder extends Seeder { }

#[SeederAfter(FeatureSeeder::class)]
class PlanSeeder extends Seeder { }

#[SeederAfter(PackageSeeder::class, PlanSeeder::class)]
class WorkspaceSeeder extends Seeder { }

// Execution order: Feature -> [Package, Plan] -> Workspace
// Package and Plan can run in either order (same priority level)

Example 3: Priority with Dependencies

// Dependencies override priority

#[SeederPriority(90)]  // High priority number (normally runs late)
#[SeederBefore(DemoSeeder::class)]
class FeatureSeeder extends Seeder { }

#[SeederPriority(10)]  // Low priority number (normally runs early)
#[SeederAfter(FeatureSeeder::class)]
class DemoSeeder extends Seeder { }

// Despite priority, FeatureSeeder runs first due to dependency

Example 4: Mixed Priority and Dependencies

// Seeders at the same dependency level sort by priority

#[SeederPriority(10)]
class FeatureSeeder extends Seeder { }

#[SeederAfter(FeatureSeeder::class)]
#[SeederPriority(20)]  // Lower priority = runs first among siblings
class PackageSeeder extends Seeder { }

#[SeederAfter(FeatureSeeder::class)]
#[SeederPriority(30)]  // Higher priority = runs after PackageSeeder
class PlanSeeder extends Seeder { }

// Order: Feature -> Package -> Plan
// (Package before Plan because 20 < 30)

Circular Dependency Errors

Circular dependencies are detected and throw CircularDependencyException.

What Causes Circular Dependencies

// This creates a cycle: A -> B -> C -> A

#[SeederAfter(SeederC::class)]
class SeederA extends Seeder { }

#[SeederAfter(SeederA::class)]
class SeederB extends Seeder { }

#[SeederAfter(SeederB::class)]
class SeederC extends Seeder { }

Error Handling

use Core\Database\Seeders\Exceptions\CircularDependencyException;

try {
    $seeders = $discovery->discover();
} catch (CircularDependencyException $e) {
    echo $e->getMessage();
    // "Circular dependency detected in seeders: SeederA -> SeederB -> SeederC -> SeederA"

    // Get the cycle chain
    $cycle = $e->cycle;
    // ['SeederA', 'SeederB', 'SeederC', 'SeederA']
}

Debugging Circular Dependencies

  1. Check the exception message for the cycle path
  2. Review the $after and $before declarations
  3. Remember that #[SeederBefore] creates implicit after relationships
  4. Use the registry to inspect relationships:
$discovery = new SeederDiscovery([app_path('Mod')]);
$seeders = $discovery->getSeeders();

foreach ($seeders as $class => $meta) {
    echo "{$class}:\n";
    echo "  Priority: {$meta['priority']}\n";
    echo "  After: " . implode(', ', $meta['after']) . "\n";
    echo "  Before: " . implode(', ', $meta['before']) . "\n";
}

Manual Registration

Use SeederRegistry for explicit control over seeder ordering:

use Core\Database\Seeders\SeederRegistry;

$registry = new SeederRegistry();

// Register with options
$registry
    ->register(FeatureSeeder::class, priority: 10)
    ->register(PackageSeeder::class, after: [FeatureSeeder::class])
    ->register(WorkspaceSeeder::class, after: [PackageSeeder::class]);

// Get ordered list
$seeders = $registry->getOrdered();

Bulk Registration

$registry->registerMany([
    FeatureSeeder::class => 10,  // Priority shorthand
    PackageSeeder::class => [
        'priority' => 50,
        'after' => [FeatureSeeder::class],
    ],
    WorkspaceSeeder::class => [
        'priority' => 50,
        'after' => [PackageSeeder::class],
        'before' => [DemoSeeder::class],
    ],
]);

Registry Operations

// Check if registered
$registry->has(FeatureSeeder::class);

// Remove a seeder
$registry->remove(DemoSeeder::class);

// Merge registries
$registry->merge($otherRegistry);

// Clear all
$registry->clear();

CoreDatabaseSeeder

Extend CoreDatabaseSeeder for automatic discovery in your application:

Basic Usage

<?php

namespace Database\Seeders;

use Core\Database\Seeders\CoreDatabaseSeeder;

class DatabaseSeeder extends CoreDatabaseSeeder
{
    // Uses auto-discovery by default
}

Custom Paths

class DatabaseSeeder extends CoreDatabaseSeeder
{
    protected function getSeederPaths(): array
    {
        return [
            app_path('Core'),
            app_path('Mod'),
            base_path('packages/my-package/src'),
        ];
    }
}

Excluding Seeders

class DatabaseSeeder extends CoreDatabaseSeeder
{
    protected function getExcludedSeeders(): array
    {
        return [
            DemoDataSeeder::class,
            TestUserSeeder::class,
        ];
    }
}

Disabling Auto-Discovery

class DatabaseSeeder extends CoreDatabaseSeeder
{
    protected bool $autoDiscover = false;

    protected function registerSeeders(SeederRegistry $registry): void
    {
        $registry
            ->register(FeatureSeeder::class, priority: 10)
            ->register(PackageSeeder::class, priority: 20)
            ->register(UserSeeder::class, priority: 30);
    }
}

Command-Line Filtering

Filter seeders when running db:seed:

# Exclude specific seeders
php artisan db:seed --exclude=DemoSeeder

# Exclude multiple
php artisan db:seed --exclude=DemoSeeder --exclude=TestSeeder

# Run only specific seeders
php artisan db:seed --only=UserSeeder

# Run multiple specific seeders
php artisan db:seed --only=UserSeeder --only=FeatureSeeder

Pattern Matching

Filters support multiple matching strategies:

# Full class name
php artisan db:seed --exclude=Mod\\Blog\\Database\\Seeders\\PostSeeder

# Short name
php artisan db:seed --exclude=PostSeeder

# Partial match
php artisan db:seed --exclude=Demo  # Matches DemoSeeder, DemoDataSeeder, etc.

Configuration

Configure the seeder system in config/core.php:

return [
    'seeders' => [
        // Enable auto-discovery
        'auto_discover' => env('CORE_SEEDER_AUTODISCOVER', true),

        // Paths to scan
        'paths' => [
            app_path('Core'),
            app_path('Mod'),
            app_path('Website'),
        ],

        // Classes to exclude
        'exclude' => [
            // App\Mod\Demo\Database\Seeders\DemoSeeder::class,
        ],
    ],
];

Best Practices

1. Use Explicit Dependencies

// Preferred: Explicit dependencies
#[SeederAfter(FeatureSeeder::class)]
class PackageSeeder extends Seeder { }

// Avoid: Relying only on priority for ordering
#[SeederPriority(51)]  // Fragile - assumes FeatureSeeder is 50
class PackageSeeder extends Seeder { }

2. Keep Seeders Focused

// Good: Single responsibility
class PostSeeder extends Seeder {
    public function run(): void {
        Post::factory()->count(50)->create();
    }
}

// Avoid: Monolithic seeders
class EverythingSeeder extends Seeder {
    public function run(): void {
        // Creates users, posts, comments, categories, tags...
    }
}

3. Use Factories in Seeders

class PostSeeder extends Seeder
{
    public function run(): void
    {
        // Good: Use factories for consistent test data
        Post::factory()
            ->count(50)
            ->has(Comment::factory()->count(3))
            ->create();
    }
}

4. Handle Idempotency

class FeatureSeeder extends Seeder
{
    public function run(): void
    {
        // Good: Use updateOrCreate for idempotent seeding
        Feature::updateOrCreate(
            ['code' => 'blog'],
            ['name' => 'Blog', 'enabled' => true]
        );
    }
}

5. Document Dependencies

/**
 * Seeds packages for the tenant module.
 *
 * Requires:
 * - FeatureSeeder: Features must exist to link packages
 * - TenantSeeder: Tenants must exist to assign packages
 */
#[SeederAfter(FeatureSeeder::class, TenantSeeder::class)]
class PackageSeeder extends Seeder { }

Troubleshooting

Seeders Not Discovered

  1. Check the file is in Database/Seeders/ subdirectory
  2. Verify class name ends with Seeder
  3. Confirm namespace matches file location
  4. Check the path is included in discovery paths

Wrong Execution Order

  1. Print discovery results to verify:
    $discovery = new SeederDiscovery([app_path('Mod')]);
    dd($discovery->getSeeders());
    
  2. Check for missing #[SeederAfter] declarations
  3. Verify priority values (lower runs first)

Circular Dependency Error

  1. Read the error message for the cycle
  2. Draw out the dependency graph
  3. Identify which relationship should be removed/reversed
  4. Consider if the circular dependency indicates a design issue

Learn More