Database Seeding

Introduction

Database seeding is the process of populating your database with initial or test data. In Slenix, seeders are simple PHP classes that insert records into your tables. They are especially useful for:

  • Populating a fresh database with the data your application needs to function (roles, categories, settings)
  • Generating large volumes of realistic fake data for development and testing
  • Sharing a reproducible dataset with your team so everyone works with the same starting point

Slenix ships with three tools for seeding:

  • Seeders — classes that orchestrate what data goes into the database
  • Factories — classes that define how to generate realistic fake data for a model
  • Fake — a static helper with 40+ methods for generating names, emails, dates, addresses, and more — no external packages needed

Directory Structure

plaintext
database/
├── migrations/         ← Schema version control
├── seeds/              ← Seeder classes
│   ├── DatabaseSeeder.php   ← Entry point (always executed by default)
│   ├── UserSeeder.php
│   └── CategorySeeder.php
└── factories/          ← Factory classes
    ├── UserFactory.php
    └── PostFactory.php

Running Seeders

Run the Default Seeder

bash
php celestial db:seed

This always runs DatabaseSeeder, which is the entry point where you orchestrate all other seeders.

Run a Specific Seeder

bash
php celestial db:seed --class=UserSeeder
php celestial db:seed --class=CategorySeeder

Fresh Migration + Seed

The most common workflow during development: drop everything, rebuild the schema, and fill with data:

bash
php celestial migrate:fresh --seed

This is equivalent to running migrate:fresh followed by db:seed.


Creating Seeders

Generate a Seeder File

bash
php celestial make:seeder UserSeeder
php celestial make:seeder CategorySeeder

Generated files are placed in database/seeds/. The name Seeder is appended automatically if omitted.

Seeder Structure

Each seeder must extend Seeder and implement the run() method:

php
<?php

declare(strict_types=1);

use Slenix\Supports\Database\Seeds\Seeder;
use Slenix\Supports\Database\Seeds\Fake;

class UserSeeder extends Seeder
{
    public function run(): void
    {
        // Your seeding logic here
    }
}

The DatabaseSeeder

DatabaseSeeder is the central coordinator. It runs when you execute db:seed without specifying a class. Register every seeder you want to run here using $this->call():

php
<?php

declare(strict_types=1);

use Slenix\Supports\Database\Seeds\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            CategorySeeder::class,   // must run before PostSeeder (FK dependency)
            UserSeeder::class,
            PostSeeder::class,
            CommentSeeder::class,
        ]);
    }
}

Order matters. Always seed parent tables before child tables. For example, seed categories before products if products has a category_id foreign key.


Writing Seeders

Inserting Data Directly

The simplest approach — hardcode the records you need:

php
class CategorySeeder extends Seeder
{
    public function run(): void
    {
        $categories = [
            ['name' => 'Technology', 'slug' => 'technology'],
            ['name' => 'Science',    'slug' => 'science'],
            ['name' => 'Business',   'slug' => 'business'],
            ['name' => 'Health',     'slug' => 'health'],
        ];

        $this->insertBatch('categories', $categories);
    }
}

Inserting Without Duplicates

Use insertOrIgnore() when running the seeder multiple times should not create duplicate records:

php
class RoleSeeder extends Seeder
{
    public function run(): void
    {
        foreach (['admin', 'editor', 'viewer'] as $role) {
            $this->insertOrIgnore('roles', [
                'name'       => $role,
                'created_at' => date('Y-m-d H:i:s'),
                'updated_at' => date('Y-m-d H:i:s'),
            ]);
        }
    }
}

Generating Fake Data with Fake

For development datasets, use the Fake helper to generate realistic random data:

php
class UserSeeder extends Seeder
{
    public function run(): void
    {
        $this->truncate('users'); // clean before seeding

        $rows = [];
        for ($i = 0; $i < 100; $i++) {
            $name    = Fake::name();
            $rows[]  = [
                'name'       => $name,
                'email'      => Fake::email($name),
                'password'   => Fake::hashedPassword('password'),
                'role'       => Fake::randomElement(['admin', 'editor', 'viewer']),
                'is_active'  => Fake::boolean(80), // 80% chance of true
                'created_at' => Fake::dateTime('-1 year'),
                'updated_at' => Fake::dateTime('-6 months'),
            ];
        }

        $inserted = $this->insertBatch('users', $rows);
        echo "  → {$inserted} users inserted\n";
    }
}

Using Models Directly

If your seeders need to trigger model events (hooks like creating, created), use the model's create() method:

php
use App\Models\User;
use App\Models\Post;

class PostSeeder extends Seeder
{
    public function run(): void
    {
        // Get all existing user IDs
        $stmt    = $this->pdo->query("SELECT id FROM users");
        $userIds = $stmt->fetchAll(\PDO::FETCH_COLUMN);

        for ($i = 0; $i < 50; $i++) {
            $title = Fake::title();
            Post::create([
                'title'      => $title,
                'slug'       => Fake::slug($title),
                'body'       => Fake::paragraph(6),
                'user_id'    => Fake::randomElement($userIds),
                'status'     => Fake::randomElement(['draft', 'published']),
                'created_at' => Fake::dateTime('-1 year'),
            ]);
        }
    }
}

Calling Other Seeders

Inside any seeder you can call other seeders using $this->call():

php
class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Call one seeder
        $this->call(UserSeeder::class);

        // Call multiple seeders in order
        $this->call([
            CategorySeeder::class,
            ProductSeeder::class,
            OrderSeeder::class,
        ]);
    }
}

Seeder API Reference

These methods are available inside every seeder via $this:

MethodDescription
insertBatch($table, $rows, $chunk)Inserts multiple rows efficiently. Default chunk: 500
insertOrIgnore($table, $data)Inserts a single row, silently skips if duplicate
truncate($table)Clears the table (disables FK checks temporarily)
exists($table, $conditions)Returns true if a matching record already exists
statement($sql, $bindings)Executes raw SQL with optional bindings
call($seederClass)Runs another seeder class or array of seeders
$this->pdoDirect access to the PDO connection for custom queries

Factories

Factories define how to generate realistic data for a model. They use the fluent API count(), state(), create(), make(), and raw().

Creating a Factory

bash
php celestial make:factory UserFactory
php celestial make:factory PostFactory

Factory Structure

php
<?php

declare(strict_types=1);

use Slenix\Supports\Database\Seeds\Factory;
use Slenix\Supports\Database\Seeds\Fake;
use App\Models\User;

class UserFactory extends Factory
{
    protected string $model = User::class;

    public function definition(): array
    {
        $name = Fake::name();

        return [
            'name'       => $name,
            'email'      => Fake::email($name),
            'password'   => Fake::hashedPassword('password'),
            'role'       => 'viewer',
            'is_active'  => true,
            'created_at' => Fake::dateTime('-1 year'),
            'updated_at' => Fake::dateTime(),
        ];
    }
}

Using Factories

php
// Create and persist 1 user (returns model instance)
$user = UserFactory::new()->create();

// Create and persist 10 users (returns array of instances)
$users = UserFactory::new()->count(10)->create();

// Override specific attributes
$admin = UserFactory::new()->create(['role' => 'admin']);

// Create without persisting to the database
$user = UserFactory::new()->make();

// Get just the attribute array (no model, no DB)
$data = UserFactory::new()->raw();
// ['name' => 'Ana Silva', 'email' => 'ana123@gmail.com', ...]

Factory States

States let you define named variations of a model with different attributes:

php
class UserFactory extends Factory
{
    protected string $model = User::class;

    public function definition(): array
    {
        return [
            'name'      => Fake::name(),
            'email'     => Fake::email(),
            'role'      => 'viewer',
            'is_active' => true,
        ];
    }

    public function admin(): static
    {
        return $this->state(['role' => 'admin']);
    }

    public function inactive(): static
    {
        return $this->state(['is_active' => false]);
    }

    public function withVerifiedEmail(): static
    {
        return $this->state(['email_verified_at' => date('Y-m-d H:i:s')]);
    }
}

Using states:

php
// Create an admin user
$admin = UserFactory::new()->admin()->create();

// Create 5 inactive users
$users = UserFactory::new()->inactive()->count(5)->create();

// Chain multiple states
$user = UserFactory::new()->admin()->withVerifiedEmail()->create();

Using Factories in Seeders

php
class UserSeeder extends Seeder
{
    public function run(): void
    {
        $this->truncate('users');

        // 1 admin
        UserFactory::new()->admin()->create();

        // 5 editors
        UserFactory::new()
            ->state(['role' => 'editor'])
            ->count(5)
            ->create();

        // 50 regular viewers
        UserFactory::new()->count(50)->create();

        echo "  → 56 users seeded\n";
    }
}

The Fake Helper

Fake is a static class with methods for generating random realistic data. No external packages needed.

Names and Contacts

php
Fake::firstName();          // 'Ana'
Fake::lastName();           // 'Silva'
Fake::name();               // 'Ana Silva'
Fake::email();              // 'ana123@gmail.com'
Fake::email('João Lima');   // 'jo.o.lima42@outlook.com'
Fake::phone();              // '(11) 98234-1234'

Internet

php
Fake::username();           // 'dev1234'
Fake::password(16);         // 'xK9mN!qR2pL#vZ8t'
Fake::hashedPassword();     // bcrypt hash of 'password'
Fake::hashedPassword('myPass123'); // bcrypt hash of 'myPass123'
Fake::url();                // 'https://tech42.io'
Fake::slug();               // 'lorem-ipsum-dolor'
Fake::slug('Meu Título');   // 'meu-titulo'
Fake::uuid();               // 'a3f9b1c2-...'

Numbers

php
Fake::numberBetween(1, 100);        // 42
Fake::decimal(0.5, 999.99, 2);      // 127.35
Fake::boolean();                    // true or false (50/50)
Fake::boolean(80);                  // true 80% of the time

Dates

php
Fake::date();                       // '2024-03-15'
Fake::date('-1 year', 'now');       // random date in last year
Fake::dateTime();                   // '2024-03-15 14:23:01'
Fake::dateTime('-6 months', '-1 week'); // within range
Fake::timestamp();                  // Unix timestamp (int)

Text

php
Fake::word();               // 'lorem'
Fake::words(4);             // 'lorem ipsum dolor sit'
Fake::sentence();           // 'Lorem ipsum dolor sit amet.'
Fake::paragraph();          // multiple sentences
Fake::text(150);            // text truncated to 150 chars
Fake::title();              // 'Lorem Ipsum Dolor Sit'

Address

php
Fake::city();               // 'São Paulo'
Fake::state();              // 'SP'
Fake::zipCode();            // '01310-100'
Fake::streetAddress();      // 'Rua Lorem, 42'

Arrays and Selection

php
Fake::randomElement(['draft', 'published', 'archived']); // 'published'
Fake::randomElements(['a', 'b', 'c', 'd'], 2);           // ['b', 'd']

Complete Example

Here is a complete seeding setup for a blog application:

bash
php celestial make:seeder DatabaseSeeder
php celestial make:seeder CategorySeeder
php celestial make:seeder UserSeeder
php celestial make:seeder PostSeeder
php celestial make:factory UserFactory
php celestial make:factory PostFactory

database/seeds/CategorySeeder.php

php
class CategorySeeder extends Seeder
{
    public function run(): void
    {
        $this->insertBatch('categories', [
            ['name' => 'Technology', 'slug' => 'technology', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s')],
            ['name' => 'Business',   'slug' => 'business',   'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s')],
            ['name' => 'Science',    'slug' => 'science',    'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s')],
        ]);
    }
}

database/seeds/UserSeeder.php

php
class UserSeeder extends Seeder
{
    public function run(): void
    {
        $this->truncate('users');

        // Always create a known admin for development login
        $this->insertOrIgnore('users', [
            'name'       => 'Admin',
            'email'      => 'admin@slenix.dev',
            'password'   => Fake::hashedPassword('password'),
            'role'       => 'admin',
            'is_active'  => true,
            'created_at' => date('Y-m-d H:i:s'),
            'updated_at' => date('Y-m-d H:i:s'),
        ]);

        // Generate 49 fake users
        $rows = [];
        for ($i = 0; $i < 49; $i++) {
            $rows[] = [
                'name'       => Fake::name(),
                'email'      => Fake::email(),
                'password'   => Fake::hashedPassword('password'),
                'role'       => Fake::randomElement(['editor', 'viewer']),
                'is_active'  => Fake::boolean(90),
                'created_at' => Fake::dateTime('-1 year'),
                'updated_at' => Fake::dateTime('-1 month'),
            ];
        }
        $this->insertBatch('users', $rows);
    }
}

database/seeds/DatabaseSeeder.php

php
class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            CategorySeeder::class,
            UserSeeder::class,
            PostSeeder::class,
        ]);
    }
}

Run everything in one command:

bash
php celestial migrate:fresh --seed

Celestial Seed Command Reference

CommandDescription
make:seeder <name>Creates a new seeder file in database/seeds/
make:factory <name>Creates a new factory file in database/factories/
db:seedRuns DatabaseSeeder
db:seed --class=<Name>Runs a specific seeder class
migrate:fresh --seedResets schema and runs all seeders