Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Backend Features/Seed System

Database Seeding Guide

Last Updated: 2026-01-29 Status: Active Audience: Developers Plan Reference: 116-comprehensive-database-seeding.md, 225-notification-workflow-seeder-refactor.md

This guide documents database seeding patterns and factories for the Client Portal application.


Table of Contents

  1. Overview
  2. Directory Structure
  3. JSON Data File System
  4. Quick Start
  5. Seeding Profiles
  6. Factories
  7. Seeders
  8. Seeding Strategies
  9. Test Data
  10. Commands
  11. Export Script

Directory Structure

Seeders are organized by purpose:

database/seeders/
├── BaseSeeder.php              # Base class with helpers
├── DatabaseSeeder.php          # Main entry point (default)
├── MasterSeeder.php            # Production orchestrator
├── DemoSeeder.php              # Demo data orchestrator
├── FullSeeder.php              # Load test orchestrator
├── MinimalSeeder.php           # Quick setup orchestrator
├── MasterAuditSeeder.php       # Audit orchestrator
├── MasterNotificationSeeder.php # Notification orchestrator
│
├── Traits/                     # Reusable seeder traits
│   └── JsonDataLoader.php      # JSON file loading & validation
│
├── System/                     # Production-required configuration
│   ├── NotificationSeeder.php  # Notification types & templates (JSON-backed)
│   ├── WorkflowSeeder.php      # Workflow definitions (JSON-backed)
│   ├── MeetingTypeSeeder.php   # Meeting types
│   └── ...
│
├── SampleData/                 # Development-only fake entities
│   ├── UserSeeder.php
│   ├── ClientSeeder.php
│   ├── ProjectSeeder.php
│   └── ...
│
├── Content/                    # Production content (blog, FAQs)
│   ├── BlogSeeder.php
│   ├── CaseStudySeeder.php
│   ├── FaqSeeder.php
│   └── audit-data/             # Audit content data files
│
├── content/                    # JSON data files for seeders (Plan 225)
│   ├── notifications/          # Notification data
│   │   ├── channels.json
│   │   ├── types.json
│   │   └── templates/          # By channel, then category
│   ├── workflows/              # Workflow data
│   │   └── definitions/        # By category
│   └── schemas/                # JSON Schema definitions
│
└── Audits/                     # Audit-related seeders
    ├── AuditCategorySeeder.php
    ├── AuditTemplateSeeder.php
    └── Phases/                 # 30 phase seeders

Namespace Mapping

FolderNamespaceUsage
RootDatabase\SeedersOrchestrators only
Traits/Database\Seeders\TraitsReusable seeder traits
System/Database\Seeders\SystemProduction config
SampleData/Database\Seeders\SampleDataDev-only entities
Content/Database\Seeders\ContentMarketing content
Audits/Database\Seeders\AuditsAudit templates
content/N/A (JSON data)JSON data files for seeders

CLI Invocation

# Orchestrators (at root)
php artisan db:seed --class=DemoSeeder

# Subfolder seeders (use full namespace)
php artisan db:seed --class="Database\\Seeders\\System\\NotificationSeeder"
php artisan db:seed --class="Database\\Seeders\\SampleData\\UserSeeder"

JSON Data File System

Plan 225: Notification and workflow seeders now load data from external JSON files instead of inline PHP arrays. This provides better maintainability, portability between environments, and schema validation.

Data File Structure

database/seeders/Content/
├── notifications/
│   ├── channels.json              # Notification channels (email, sms, slack, in_app)
│   ├── types.json                 # 60+ notification types
│   └── templates/
│       ├── email/
│       │   ├── leads.json         # Lead/discovery email templates
│       │   ├── billing.json       # Invoice/payment email templates
│       │   └── ...
│       ├── in_app/
│       │   └── general.json       # In-app notification templates
│       ├── sms/
│       │   └── ...
│       └── slack/
│           └── ...
├── workflows/
│   └── definitions/
│       ├── billing.json           # Billing workflow definitions
│       ├── projects.json          # Project workflow definitions
│       ├── communication.json     # Meeting/communication workflows
│       └── ...
└── schemas/
    ├── channels.json              # JSON Schema for channels
    ├── types.json                 # JSON Schema for notification types
    ├── templates.json             # JSON Schema for templates
    ├── workflows.json             # JSON Schema for workflows
    └── action_config.json         # JSON Schema for workflow action config

JSON File Format

All JSON data files use a wrapper format with metadata:

{
    "_meta": {
        "generated_at": "2026-01-29T12:00:00+00:00",
        "source_environment": "production",
        "command": "php scripts/data/export-notification-workflow-seeders.php --env=production",
        "warning": "AUTO-GENERATED - Manual edits will be overwritten on next export",
        "record_count": 62,
        "checksum": "sha256:abc123..."
    },
    "data": [
        { ... },
        { ... }
    ]
}

JsonDataLoader Trait

Seeders use the JsonDataLoader trait for loading and validating JSON files:

<?php

namespace Database\Seeders\System;

use Database\Seeders\BaseSeeder;
use Database\Seeders\Traits\JsonDataLoader;

class NotificationSeeder extends BaseSeeder
{
    use JsonDataLoader;

    public function run(): void
    {
        // Load with schema validation
        $types = $this->loadAndValidateJson('notifications/types.json', 'types');

        // Load from nested directories
        $templates = $this->loadJsonNestedDirectory('notifications/templates', 'templates');

        // Load with checksum verification
        $channels = $this->loadJsonWithChecksum('notifications/channels.json', true);

        // Load simple (no validation)
        $data = $this->loadJsonData('notifications/channels.json');
    }
}

Trait Methods

MethodDescription
loadJsonData($path)Load JSON file, extract data array
loadAndValidateJson($path, $schema)Load and validate against JSON Schema
loadJsonWithChecksum($path, $verify)Load with optional checksum verification
loadJsonDirectory($dir, $schema)Load all JSON files from directory
loadJsonNestedDirectory($dir, $schema)Load from nested subdirectories

Fallback Behavior

Both NotificationSeeder and WorkflowSeeder support fallback to inline data:

// In NotificationSeeder.php
private function jsonDataFilesExist(): bool
{
    return File::exists(database_path('seeders/Content/notifications/channels.json'))
        && File::exists(database_path('seeders/Content/notifications/types.json'));
}

public function run(): void
{
    if ($this->jsonDataFilesExist()) {
        $this->progress('Loading notification data from JSON files');
        $channels = $this->loadAndValidateJson('notifications/channels.json', 'channels');
    } else {
        $this->progress('JSON data files not found - using inline data');
        $channels = $this->getInlineChannels();
    }
}

Schema Validation

JSON Schemas use absolute URIs and validate structure:

{
    "$schema": "https://json-schema.org/draft-07/schema#",
    "$id": "https://scopeforged.com/schemas/seeder/types.json",
    "type": "array",
    "items": {
        "type": "object",
        "required": ["key", "name", "category", "default_channels"],
        "properties": {
            "key": {
                "type": "string",
                "pattern": "^[a-z][a-z0-9_.]*$"
            },
            "name": { "type": "string", "minLength": 1 },
            "category": { "type": "string" },
            "default_channels": {
                "type": "array",
                "items": { "enum": ["email", "sms", "slack", "in_app", "push"] }
            }
        }
    }
}

Concurrent Seeding Protection

Seeders use advisory locking to prevent concurrent execution:

private const LOCK_KEY = 'seeder:notifications';
private const LOCK_TIMEOUT = 300;

public function run(): void
{
    $lock = Cache::lock(self::LOCK_KEY, self::LOCK_TIMEOUT);

    if (! $lock->get()) {
        $this->warn('Another seeder is running. Waiting...');
        $lock->block(self::LOCK_TIMEOUT);
    }

    try {
        DB::transaction(function () {
            // Seed data atomically
        });
    } finally {
        $lock->release();
    }
}

Upsert by Key

Workflows and templates use a key column for stable upserts:

// Migration adds key column
Schema::table('workflows', function (Blueprint $table) {
    $table->string('key')->nullable()->unique()->after('id');
});

// Seeder upserts by key
if ($key) {
    $workflow = Workflow::updateOrCreate(
        ['key' => $key],
        $workflowData
    );
} else {
    // Legacy: upsert by name
    $workflow = Workflow::updateOrCreate(
        ['name' => $workflowDef['name']],
        $workflowData
    );
}

Backfilling Keys

Use the backfill script to generate keys for existing records:

# Dry run (preview changes)
php scripts/data/backfill-seeder-keys.php --env=production --dry-run

# Apply changes
php scripts/data/backfill-seeder-keys.php --env=production

# Force (skip confirmation)
php scripts/data/backfill-seeder-keys.php --env=production --force

Quick Start

# Fresh database with standard seeding
php artisan migrate:fresh --seed

# Quick development setup
php artisan db:seed --class=MinimalSeeder

# Full demo data
php artisan db:seed --class=DemoSeeder

# Load testing data
php artisan db:seed --class=FullSeeder

Default Login Credentials:

RoleEmailPassword
Adminadmin@example.compassword
Clientclient@example.compassword

Seeding Profiles

The application provides multiple seeding profiles for different use cases:

Minimal (Quick Development)

php artisan db:seed --class=MinimalSeeder

Creates minimal data for quick development setup:

  • 2 users (1 admin, 1 client)
  • 2 clients with 2 projects each
  • 2 invoices

Standard (Default)

php artisan db:seed
# or
php artisan migrate:fresh --seed

Creates comprehensive data for testing:

  • 6 users (1 admin, 5 clients)
  • 5 client organizations
  • 13 projects with tasks and files
  • 16 invoices with line items
  • Project plans and milestones
  • Conversations and messages
  • Dashboards and analytics
  • Workflow templates
  • Activity logs

Demo (Sales Presentations)

php artisan db:seed --class=DemoSeeder

Creates rich demo data for presentations:

  • All standard data
  • Named demo clients (Acme Corp, TechStart, etc.)
  • Active conversations and document requests
  • Impressive dashboard visualizations
  • Complete project lifecycle examples

Full (Load Testing)

php artisan db:seed --class=FullSeeder

Creates large dataset for performance testing:

  • 20 clients with 3+ projects each
  • 60+ projects total
  • Thousands of time entries
  • Heavy analytics data

Overview

Database seeding provides:

  • Development environment setup
  • Testing with realistic data
  • Demo/staging data population
  • Initial production data (roles, settings)

Seeding Components

ComponentPurpose
FactoriesGenerate model instances with fake data
SeedersOrchestrate data creation
FakerGenerate realistic fake data

Factories

Generate a Factory

php artisan make:factory ClientFactory
php artisan make:factory InvoiceFactory --model=Invoice

Basic Factory Structure

<?php

namespace Database\Factories;

use App\Enums\ClientStatus;
use App\Models\Client;
use Illuminate\Database\Eloquent\Factories\Factory;

class ClientFactory extends Factory
{
    protected $model = Client::class;

    public function definition(): array
    {
        return [
            'company_name' => fake()->company(),
            'contact_name' => fake()->name(),
            'email' => fake()->unique()->companyEmail(),
            'phone' => fake()->phoneNumber(),
            'address' => fake()->address(),
            'city' => fake()->city(),
            'state' => fake()->stateAbbr(),
            'postal_code' => fake()->postcode(),
            'country' => 'US',
            'status' => fake()->randomElement(ClientStatus::cases()),
            'notes' => fake()->optional(0.3)->paragraph(),
        ];
    }
}

Factory States

<?php

namespace Database\Factories;

use App\Enums\ClientStatus;
use App\Models\Client;
use Illuminate\Database\Eloquent\Factories\Factory;

class ClientFactory extends Factory
{
    public function definition(): array
    {
        return [
            'company_name' => fake()->company(),
            'contact_name' => fake()->name(),
            'email' => fake()->unique()->companyEmail(),
            'status' => ClientStatus::Active,
        ];
    }

    /**
     * Active client state.
     */
    public function active(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => ClientStatus::Active,
        ]);
    }

    /**
     * Inactive client state.
     */
    public function inactive(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => ClientStatus::Inactive,
        ]);
    }

    /**
     * Client with pending approval.
     */
    public function pending(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => ClientStatus::Pending,
            'approved_at' => null,
        ]);
    }

    /**
     * VIP client with special terms.
     */
    public function vip(): static
    {
        return $this->state(fn (array $attributes) => [
            'is_vip' => true,
            'discount_rate' => 0.15,
            'payment_terms' => 60,
        ]);
    }

    /**
     * Client with complete address.
     */
    public function withFullAddress(): static
    {
        return $this->state(fn (array $attributes) => [
            'address' => fake()->streetAddress(),
            'city' => fake()->city(),
            'state' => fake()->stateAbbr(),
            'postal_code' => fake()->postcode(),
            'country' => fake()->country(),
        ]);
    }
}

// Usage
Client::factory()->active()->create();
Client::factory()->vip()->withFullAddress()->create();
Client::factory()->inactive()->count(5)->create();

Factory Relationships

<?php

namespace Database\Factories;

use App\Enums\InvoiceStatus;
use App\Models\Client;
use App\Models\Invoice;
use Illuminate\Database\Eloquent\Factories\Factory;

class InvoiceFactory extends Factory
{
    public function definition(): array
    {
        $subtotal = fake()->randomFloat(2, 100, 10000);
        $taxRate = 0.10;
        $taxAmount = $subtotal * $taxRate;

        return [
            'client_id' => Client::factory(),
            'number' => 'INV-' . fake()->unique()->numerify('######'),
            'status' => InvoiceStatus::Draft,
            'issue_date' => fake()->dateTimeBetween('-30 days', 'now'),
            'due_date' => fn (array $attributes) =>
                $attributes['issue_date']->modify('+30 days'),
            'subtotal' => $subtotal,
            'tax_rate' => $taxRate,
            'tax_amount' => $taxAmount,
            'total' => $subtotal + $taxAmount,
            'notes' => fake()->optional(0.5)->sentence(),
        ];
    }

    /**
     * Invoice that's been sent.
     */
    public function sent(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => InvoiceStatus::Sent,
            'sent_at' => now(),
        ]);
    }

    /**
     * Paid invoice.
     */
    public function paid(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => InvoiceStatus::Paid,
            'sent_at' => now()->subDays(15),
            'paid_at' => now()->subDays(5),
            'payment_method' => fake()->randomElement(['card', 'bank_transfer', 'check']),
        ]);
    }

    /**
     * Overdue invoice.
     */
    public function overdue(): static
    {
        return $this->state(fn (array $attributes) => [
            'status' => InvoiceStatus::Overdue,
            'issue_date' => now()->subDays(45),
            'due_date' => now()->subDays(15),
            'sent_at' => now()->subDays(45),
        ]);
    }

    /**
     * Invoice for a specific client.
     */
    public function forClient(Client $client): static
    {
        return $this->state(fn (array $attributes) => [
            'client_id' => $client->id,
        ]);
    }

    /**
     * Create invoice with line items.
     */
    public function withItems(int $count = 3): static
    {
        return $this->afterCreating(function (Invoice $invoice) use ($count) {
            InvoiceItem::factory()
                ->count($count)
                ->forInvoice($invoice)
                ->create();

            $invoice->recalculateTotals();
        });
    }
}

// Usage
Invoice::factory()->sent()->create();
Invoice::factory()->paid()->forClient($client)->create();
Invoice::factory()->withItems(5)->create();

Factory Sequences

use Illuminate\Database\Eloquent\Factories\Sequence;

// Alternate between states
Invoice::factory()
    ->count(6)
    ->state(new Sequence(
        ['status' => InvoiceStatus::Draft],
        ['status' => InvoiceStatus::Sent],
        ['status' => InvoiceStatus::Paid],
    ))
    ->create();

// Sequential values
Client::factory()
    ->count(3)
    ->state(new Sequence(
        ['company_name' => 'Alpha Corp'],
        ['company_name' => 'Beta Inc'],
        ['company_name' => 'Gamma LLC'],
    ))
    ->create();

// With index access
Invoice::factory()
    ->count(5)
    ->state(new Sequence(
        fn (Sequence $sequence) => [
            'number' => 'INV-' . str_pad($sequence->index + 1, 6, '0', STR_PAD_LEFT),
        ]
    ))
    ->create();

Factory Callbacks

class ClientFactory extends Factory
{
    public function definition(): array
    {
        return [
            'company_name' => fake()->company(),
            // ...
        ];
    }

    /**
     * Configure the factory.
     */
    public function configure(): static
    {
        return $this->afterMaking(function (Client $client) {
            // Called after make() but before save
            $client->slug = Str::slug($client->company_name);
        })->afterCreating(function (Client $client) {
            // Called after save
            activity()->log("Client {$client->company_name} created via factory");
        });
    }
}

Seeders

Generate a Seeder

php artisan make:seeder ClientSeeder
php artisan make:seeder DemoDataSeeder

Basic Seeder Structure

<?php

namespace Database\Seeders;

use App\Models\Client;
use Illuminate\Database\Seeder;

class ClientSeeder extends Seeder
{
    public function run(): void
    {
        // Create specific clients
        Client::factory()->create([
            'company_name' => 'Acme Corporation',
            'email' => 'contact@acme.com',
        ]);

        // Create random clients
        Client::factory()
            ->count(20)
            ->create();

        // Create clients with relationships
        Client::factory()
            ->count(5)
            ->has(Project::factory()->count(3))
            ->has(Invoice::factory()->count(2))
            ->create();
    }
}

Database Seeder (Main Orchestrator)

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Production-safe seeders (roles, permissions, settings)
        $this->call([
            RoleSeeder::class,
            PermissionSeeder::class,
            SettingsSeeder::class,
        ]);

        // Development/staging only
        if (app()->environment(['local', 'staging'])) {
            $this->call([
                AdminUserSeeder::class,
                ClientSeeder::class,
                ProjectSeeder::class,
                InvoiceSeeder::class,
            ]);
        }
    }
}

Specialized Seeders

<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class AdminUserSeeder extends Seeder
{
    public function run(): void
    {
        // Only create if doesn't exist
        User::firstOrCreate(
            ['email' => 'admin@clientportal.test'],
            [
                'name' => 'Admin User',
                'password' => Hash::make('password'),
                'email_verified_at' => now(),
                'is_admin' => true,
            ]
        );

        User::firstOrCreate(
            ['email' => 'client@clientportal.test'],
            [
                'name' => 'Demo Client',
                'password' => Hash::make('password'),
                'email_verified_at' => now(),
                'is_admin' => false,
            ]
        );
    }
}
<?php

namespace Database\Seeders;

use App\Enums\InvoiceStatus;
use App\Models\Client;
use App\Models\Invoice;
use Illuminate\Database\Seeder;

class InvoiceSeeder extends Seeder
{
    public function run(): void
    {
        $clients = Client::all();

        if ($clients->isEmpty()) {
            $this->command->warn('No clients found. Run ClientSeeder first.');
            return;
        }

        // Create invoices for each client
        $clients->each(function (Client $client) {
            // Draft invoices
            Invoice::factory()
                ->count(2)
                ->forClient($client)
                ->create();

            // Sent invoices
            Invoice::factory()
                ->count(3)
                ->sent()
                ->forClient($client)
                ->create();

            // Paid invoices
            Invoice::factory()
                ->count(5)
                ->paid()
                ->forClient($client)
                ->create();

            // One overdue invoice
            Invoice::factory()
                ->overdue()
                ->forClient($client)
                ->create();
        });

        $this->command->info('Created invoices for ' . $clients->count() . ' clients.');
    }
}

Seeding Strategies

Production-Safe Seeding

<?php

namespace Database\Seeders;

use App\Models\Role;
use Illuminate\Database\Seeder;

class RoleSeeder extends Seeder
{
    public function run(): void
    {
        $roles = [
            ['name' => 'admin', 'display_name' => 'Administrator'],
            ['name' => 'manager', 'display_name' => 'Manager'],
            ['name' => 'client', 'display_name' => 'Client'],
        ];

        foreach ($roles as $role) {
            Role::firstOrCreate(
                ['name' => $role['name']],
                $role
            );
        }
    }
}

Idempotent Seeding

<?php

namespace Database\Seeders;

use App\Models\Setting;
use Illuminate\Database\Seeder;

class SettingsSeeder extends Seeder
{
    public function run(): void
    {
        $settings = [
            'company_name' => 'Client Portal',
            'invoice_prefix' => 'INV-',
            'invoice_due_days' => 30,
            'tax_rate' => 0.10,
            'currency' => 'USD',
        ];

        foreach ($settings as $key => $value) {
            Setting::updateOrCreate(
                ['key' => $key],
                ['value' => $value]
            );
        }
    }
}

Demo Data Seeder

<?php

namespace Database\Seeders;

use App\Models\Client;
use App\Models\Invoice;
use App\Models\Project;
use App\Models\User;
use Illuminate\Database\Seeder;

class DemoDataSeeder extends Seeder
{
    public function run(): void
    {
        $this->command->info('Creating demo data...');

        // Create demo admin
        $admin = User::factory()->create([
            'name' => 'Demo Admin',
            'email' => 'demo@example.com',
            'is_admin' => true,
        ]);

        // Create clients with full data
        $clients = Client::factory()
            ->count(10)
            ->create();

        $bar = $this->command->getOutput()->createProgressBar($clients->count());

        $clients->each(function (Client $client) use ($bar) {
            // Create user for client
            $user = User::factory()->create([
                'email' => "client-{$client->id}@example.com",
            ]);
            $client->update(['user_id' => $user->id]);

            // Create projects
            $projects = Project::factory()
                ->count(rand(1, 5))
                ->forClient($client)
                ->create();

            // Create invoices
            Invoice::factory()
                ->count(rand(3, 10))
                ->withItems(rand(2, 5))
                ->forClient($client)
                ->create();

            $bar->advance();
        });

        $bar->finish();
        $this->command->newLine();
        $this->command->info('Demo data created successfully!');
    }
}

Truncate Before Seeding

<?php

namespace Database\Seeders;

use App\Models\Client;
use App\Models\Invoice;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class FreshDataSeeder extends Seeder
{
    public function run(): void
    {
        // Disable foreign key checks
        DB::statement('SET FOREIGN_KEY_CHECKS=0');

        // Truncate tables
        Invoice::truncate();
        Client::truncate();

        // Re-enable foreign key checks
        DB::statement('SET FOREIGN_KEY_CHECKS=1');

        // Seed fresh data
        $this->call([
            ClientSeeder::class,
            InvoiceSeeder::class,
        ]);
    }
}

Test Data

Using Factories in Tests

<?php

namespace Tests\Feature;

use App\Models\Client;
use App\Models\Invoice;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class InvoiceTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_view_client_invoices(): void
    {
        $client = Client::factory()
            ->has(Invoice::factory()->count(5))
            ->create();

        $user = User::factory()->admin()->create();

        $response = $this->actingAs($user)
            ->get(route('admin.clients.invoices', $client));

        $response->assertOk();
        $response->assertViewHas('invoices');
    }

    public function test_overdue_invoices_are_highlighted(): void
    {
        $invoice = Invoice::factory()->overdue()->create();

        $this->assertTrue($invoice->isOverdue());
        $this->assertEquals('overdue', $invoice->status->value);
    }

    public function test_invoice_total_calculation(): void
    {
        $invoice = Invoice::factory()
            ->withItems(3)
            ->create();

        $expectedSubtotal = $invoice->items->sum('total');
        $expectedTax = $expectedSubtotal * $invoice->tax_rate;
        $expectedTotal = $expectedSubtotal + $expectedTax;

        $this->assertEquals($expectedSubtotal, $invoice->subtotal);
        $this->assertEquals($expectedTotal, $invoice->total);
    }
}

Factory Helpers in Tests

<?php

namespace Tests;

use App\Models\User;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    protected function createAdmin(): User
    {
        return User::factory()->admin()->create();
    }

    protected function createClientUser(): User
    {
        return User::factory()
            ->hasClient()
            ->create();
    }

    protected function actingAsAdmin(): static
    {
        return $this->actingAs($this->createAdmin());
    }
}

Commands

Running Seeders

# Run all seeders
php artisan db:seed

# Run specific seeder
php artisan db:seed --class=ClientSeeder

# Fresh migration + seed
php artisan migrate:fresh --seed

# Seed in production (requires confirmation)
php artisan db:seed --force

Custom Seed Command

<?php

namespace App\Console\Commands;

use Database\Seeders\DemoDataSeeder;
use Illuminate\Console\Command;

class SeedDemoData extends Command
{
    protected $signature = 'app:seed-demo
        {--fresh : Truncate tables before seeding}
        {--clients=10 : Number of clients to create}';

    protected $description = 'Seed demo data for development';

    public function handle(): int
    {
        if (app()->environment('production')) {
            $this->error('Cannot run demo seeder in production!');
            return Command::FAILURE;
        }

        if ($this->option('fresh')) {
            $this->call('migrate:fresh');
        }

        $this->call('db:seed', [
            '--class' => DemoDataSeeder::class,
        ]);

        $this->info('Demo data seeded successfully!');

        return Command::SUCCESS;
    }
}

Best Practices

Do

  • Use factories for all model creation in tests
  • Create meaningful factory states
  • Make seeders idempotent (safe to run multiple times)
  • Separate production and development seeders
  • Use firstOrCreate for required data
  • Provide realistic fake data
  • Document factory states

Don't

  • Hardcode IDs in factories
  • Create circular dependencies
  • Run development seeders in production
  • Skip factories and create models manually
  • Forget to handle relationships
  • Use random data for required lookups

Factory Checklist

  • All model attributes defined
  • Appropriate states for common scenarios
  • Relationships properly configured
  • Sequences for unique values
  • Callbacks for complex setup
  • Used in tests consistently

Export Script

Plan 225: The export-notification-workflow-seeders.php script exports notification and workflow data from the database to JSON files.

Usage

# Export all data from production
php scripts/data/export-notification-workflow-seeders.php --env=production

# Export only notifications
php scripts/data/export-notification-workflow-seeders.php --env=production --notifications-only

# Export only workflows
php scripts/data/export-notification-workflow-seeders.php --env=production --workflows-only

# Dry run (show what would be exported)
php scripts/data/export-notification-workflow-seeders.php --env=production --dry-run

# Compare database with existing files
php scripts/data/export-notification-workflow-seeders.php --env=production --compare

# Validate database integrity before export
php scripts/data/export-notification-workflow-seeders.php --env=production --validate

# Skip confirmation prompts (for CI/CD)
php scripts/data/export-notification-workflow-seeders.php --env=production --force

Script Options

FlagDescription
--env=<environment>Override the application environment
--notifications-onlyExport only notification data
--workflows-onlyExport only workflow data
--dry-runShow what would be exported without writing
--compareCompare database state with existing files
--validateValidate database integrity before export
--forceSkip confirmation prompts

Output

The script exports to:

  • database/seeders/Content/notifications/channels.json
  • database/seeders/Content/notifications/types.json
  • database/seeders/Content/notifications/templates/{channel}/{category}.json
  • database/seeders/Content/workflows/definitions/{category}.json

Example Output

===========================================
  Export Notification & Workflow Seeders
  Plan 225
===========================================

Environment: production
Database: client_portal
Mode: EXPORT

Exporting Notifications...
--------------------------------------------------
  Written: database/seeders/Content/notifications/channels.json
  Written: database/seeders/Content/notifications/types.json
  Written: database/seeders/Content/notifications/templates/email/leads.json
  Written: database/seeders/Content/notifications/templates/email/billing.json
  ...

Exporting Workflows...
--------------------------------------------------
  Written: database/seeders/Content/workflows/definitions/billing.json
  Written: database/seeders/Content/workflows/definitions/projects.json
  ...

===========================================
  Summary
===========================================

Export complete!
Files written: 60
Total records: 322

Workflow: Syncing Environments

  1. Export from production:

    php scripts/data/export-notification-workflow-seeders.php --env=production --force
    
  2. Commit JSON files:

    git add database/seeders/Content/
    git commit -m "Export notification/workflow data from production"
    
  3. On staging/development, run seeders:

    php artisan db:seed --class="Database\\Seeders\\System\\NotificationSeeder"
    php artisan db:seed --class="Database\\Seeders\\System\\WorkflowSeeder"
    

Compare Mode

Compare database with existing files without making changes:

php scripts/data/export-notification-workflow-seeders.php --env=production --compare

Output:

Channels:
  No changes detected.

Types:
  Added (2):
    + new.notification.type
    + another.new.type
  Modified (1):
    ~ existing.type

Workflows (billing):
  Removed (1):
    - old.workflow.key