Claude Code Plugins

Community-maintained marketplace

Feedback

Idempotent seeder patterns with core/reference/dev separation and environment-specific execution using firstOrCreate/updateOrCreate

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name Database Seeding
description Idempotent seeder patterns with core/reference/dev separation and environment-specific execution using firstOrCreate/updateOrCreate
allowed-tools Read, Write, Edit, Grep, Glob, Bash

Database Seeding

Create and manage idempotent database seeders following PCR Card's core/reference/dev pattern.

When to Use

  • Creating new seeders
  • Resetting database with test data
  • Seeding specific data sets (core, reference, development)
  • Understanding seeder organization
  • Troubleshooting seeding issues

Quick Commands

# Full database reset with all seeders
./scripts/dev.sh fresh

# Core seeders only (all environments)
./scripts/dev.sh seed:core

# Reference data (trading card catalog)
./scripts/dev.sh seed:reference

# Development test data
./scripts/dev.sh seed:dev

# Specific seeder class
php artisan db:seed --class=MenuSeeder
php artisan db:seed --class=Database\\Seeders\\Development\\DuskTestSeeder

Idempotent Pattern

Critical Rule: All seeders must be safe to run multiple times.

Use firstOrCreate() or updateOrCreate()

// ✅ CORRECT: Idempotent (safe to re-run)
User::firstOrCreate(
    ['email' => 'admin@pcrcard.com'],
    [
        'name' => 'PCR Card Admin',
        'password' => Hash::make('admin123!'),
    ]
);

// ✅ CORRECT: Updates existing records
Service::updateOrCreate(
    ['service_code' => 'BASE_RESTORATION'],
    [
        'name' => 'Base Restoration',
        'base_price' => 3500,
        'is_active' => true,
    ]
);

❌ NEVER Use create()

// ❌ WRONG: Not idempotent (fails on second run)
User::create([
    'email' => 'admin@pcrcard.com',
    'name' => 'PCR Card Admin',
    'password' => Hash::make('admin123!'),
]);
// Second run: SQLSTATE[23000]: Integrity constraint violation

Seeder Organization

Three Categories

Category Purpose When to Run Example
Core Essential data for all environments Always Roles, Permissions, Services, Admin User
Reference Static catalog data Optional Trading card sets, card catalog
Development Test data for local development Local only Dusk test users, sample submissions

Directory Structure

database/seeders/
├── DatabaseSeeder.php           # Main orchestrator
├── Core/                        # Core seeders (run in all envs)
│   ├── CoreRolesAndPermissionsSeeder.php
│   ├── CoreServicesSeeder.php
│   ├── CoreAdminUserSeeder.php
│   └── MenuSeeder.php
├── Reference/                   # Reference data (optional)
│   ├── TradingCardSetsSeeder.php
│   └── TradingCardsSeeder.php
└── Development/                 # Dev-only test data
    ├── DuskTestSeeder.php
    ├── DevSubmissionsSeeder.php
    └── DevSubmissionImagesSeeder.php

Core Seeders

Always Run (All Environments)

1. CoreRolesAndPermissionsSeeder

  • 3 roles: Admin, Technician, Customer
  • 39 permissions across 11 resources
  • Uses Spatie Permission package

2. CoreServicesSeeder

  • 10 baseline services (restoration types, addons, shipping)
  • Base prices and service codes
  • Active/inactive status

3. CoreAdminUserSeeder

  • admin@pcrcard.com with Admin + Technician roles
  • Password: admin123! (development)
  • Email verified, beta access granted

4. MenuSeeder

  • 29 Nova admin menu items
  • 5 groups: Dashboards, Library, Workflow, Payments, System
  • Hierarchical structure (3 levels)

Run via:

./scripts/dev.sh seed:core
# Or manually:
php artisan db:seed --class=Database\\Seeders\\Core\\CoreRolesAndPermissionsSeeder
php artisan db:seed --class=Database\\Seeders\\Core\\CoreServicesSeeder
php artisan db:seed --class=Database\\Seeders\\Core\\CoreAdminUserSeeder
php artisan db:seed --class=MenuSeeder

Reference Data Seeders

Optional Catalog Data

TradingCardSetsSeeder - Card set catalog TradingCardsSeeder - Individual card catalog

When to use: If you need realistic trading card data for testing submissions

Run via:

./scripts/dev.sh seed:reference

Development Seeders

Test Data (Local Only)

DuskTestSeeder - Browser test users:

DevSubmissionsSeeder - Sample submissions with cards

DevSubmissionImagesSeeder - Submission-level images:

  • Package arrival photos
  • Batch photography
  • Shipping documentation

Run via:

./scripts/dev.sh seed:dev

Seeder Output Format

Structured Summary

After seeding, DatabaseSeeder provides:

╔═══════════════════════════════════════════════════════════════╗
║                 DEVELOPMENT ENVIRONMENT SEEDING                ║
╔═══════════════════════════════════════════════════════════════╝

📋 CORE SEEDERS (Always Run)
  ✅ Roles & Permissions
  ✅ Services
  ✅ Admin User
  ✅ Nova Menus

📚 REFERENCE DATA (Included)
  ✅ Trading Card Sets
  ✅ Trading Cards

🔧 DEVELOPMENT SEEDERS (Environment-Specific)
  ✅ Dusk Test Users
  ✅ Sample Submissions
  ✅ Submission Images

✅ Seeding completed successfully!

📝 MANUAL COMMANDS AVAILABLE:
  php artisan db:seed --class=Database\\Seeders\\Development\\DuskTestSeeder
  php artisan db:seed --class=Database\\Seeders\\Development\\DevSubmissionsSeeder

Environment-Specific Execution

DatabaseSeeder Logic

public function run(): void
{
    // CORE: Always run
    $this->call([
        CoreRolesAndPermissionsSeeder::class,
        CoreServicesSeeder::class,
        CoreAdminUserSeeder::class,
        MenuSeeder::class,
    ]);

    // REFERENCE: Optional (flag-controlled)
    if ($this->includeReferenceData()) {
        $this->call([
            TradingCardSetsSeeder::class,
            TradingCardsSeeder::class,
        ]);
    }

    // DEVELOPMENT: Local only
    if (App::environment('local')) {
        $this->call([
            DuskTestSeeder::class,
            DevSubmissionsSeeder::class,
            DevSubmissionImagesSeeder::class,
        ]);
    }

    // STAGING: Staging-specific seeders
    if (App::environment('staging')) {
        // Staging seeders here
    }

    // PRODUCTION: Production-specific seeders
    if (App::environment('production')) {
        // Production seeders here (usually none)
    }
}

Creating New Seeders

Template

<?php

namespace Database\Seeders\Core; // or Development, Reference

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

class YourSeeder extends Seeder
{
    public function run(): void
    {
        // Use firstOrCreate for unique records
        YourModel::firstOrCreate(
            ['unique_field' => 'value'],
            [
                'other_field' => 'value',
                'created_at' => now(),
            ]
        );

        // Use updateOrCreate to update existing
        YourModel::updateOrCreate(
            ['unique_field' => 'value'],
            [
                'other_field' => 'new_value',
                'updated_at' => now(),
            ]
        );

        // For multiple records
        $records = [
            ['name' => 'Record 1', 'code' => 'REC1'],
            ['name' => 'Record 2', 'code' => 'REC2'],
        ];

        foreach ($records as $record) {
            YourModel::updateOrCreate(
                ['code' => $record['code']],
                $record
            );
        }

        $this->command->info('✅ YourModel seeded successfully');
    }
}

Artisan Command

# Create seeder
php artisan make:seeder YourSeeder

# Move to appropriate directory
mv database/seeders/YourSeeder.php database/seeders/Core/
# or Development/, Reference/

Common Patterns

Seeding with Relationships

// Create parent with children
$submission = Submission::firstOrCreate(
    ['submission_number' => 'SUB-001'],
    ['user_id' => $user->id, 'status' => 'draft']
);

// Attach related models
$submission->cards()->firstOrCreate(
    ['card_number' => 1],
    ['description' => 'Card 1']
);

Seeding with Factories (Development Only)

// In Development seeders, you can use factories
if (App::environment('local')) {
    User::factory()->count(10)->create();
}

Conditional Seeding

// Only seed if table is empty
if (Service::count() === 0) {
    // Seed services
}

// Or use firstOrCreate (handles this automatically)

Common Pitfalls

❌ WRONG: Using create() instead of firstOrCreate()

Role::create(['name' => 'Admin']);
// Second run: duplicate key error

✅ CORRECT:

Role::firstOrCreate(['name' => 'Admin']);
// Second run: no error, returns existing record

❌ WRONG: Not specifying unique keys

Service::updateOrCreate([
    'name' => 'Base Restoration',
    'base_price' => 3500,
]);
// Creates duplicate if name changed

✅ CORRECT: Use service_code as unique identifier

Service::updateOrCreate(
    ['service_code' => 'BASE_RESTORATION'],
    [
        'name' => 'Base Restoration',
        'base_price' => 3500,
    ]
);

❌ WRONG: Hardcoding IDs

Submission::create([
    'user_id' => 1,  // Assumes user ID is 1
]);

✅ CORRECT: Look up by unique field

$user = User::where('email', 'admin@pcrcard.com')->first();
Submission::firstOrCreate([
    'submission_number' => 'SUB-001',
    'user_id' => $user->id,
]);

Testing Seeders

Verify Idempotence

# Run seeder twice
php artisan db:seed --class=YourSeeder
php artisan db:seed --class=YourSeeder

# Should complete without errors
# Check record count hasn't doubled
php artisan tinker
>>> YourModel::count();

Reset and Reseed

# Full reset
./scripts/dev.sh fresh

# Check all seeders ran successfully
php artisan tinker
>>> User::where('email', 'admin@pcrcard.com')->exists();
=> true
>>> Service::count();
=> 10
>>> MenuItem::count();
=> 29

Documentation Links

  • Database Seeders Guide: docs/DATABASE-SEEDERS.md
  • Service Architecture: docs/SERVICE-ARCHITECTURE.md
  • Nova Menus Guide: docs/features/NOVA-MENUS-GUIDE.md
  • Submission Images Guide: docs/features/SUBMISSION-IMAGES-GUIDE.md
  • Laravel Seeding: https://laravel.com/docs/12.x/seeding