Laravel Seeders on Steroids

laravel
Table of Contents

It's very common for Laravel developers to create "seeder" classes that run in their local environment. The normal use-case is to create some dummy data for local development using Factory classes or the DB facade.

Here are some commands that you've probably run before:

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

Recently I learned that seeders can be more than just Factory runners.

Looking at the underlying Seeder class, you'll notice a couple of different properties:

namespace Illuminate\Database;

abstract class Seeder
{
    /**
     * The container instance.
     *
     * @var \Illuminate\Container\Container
     */
    protected $container;

    /**
     * The console command instance.
     *
     * @var \Illuminate\Console\Command
     */
    protected $command;

    // More stuff here...
}

The $container property can be found in lots of classes provided by Laravel. It's the main way to automatically resolve dependencies, register bindings, etc.

If you want to read more about Laravel's container, visit the official documentation.

When you run a Seeder class via one of the many artisan commands, the $container property will actually be used to call the run() method defined on the class.

This means you can type-hint container-bound dependencies in your run method's signature to help with extra tasks.

class ExternalDataSeeder extends Seeder
{
    public function run(ExternalServiceClient $service)
    {
        $rates = $service->getRates();

        foreach ($rates as $rate) {
            // Do something with the rates here...
        }
    }
}

The ExternalServiceClient could be any class or object that has been bound to the container inside of a ServiceProvider or provided by a third-party package.

Dependency injection is a perfect way of removing the construction and resolution logic from the body of the run method.

I think $command is my favourite of the two. As the DocBlock suggests, it holds an instance of Illuminate\Console\Command.

When you're running your seeders via artisan, this is likely going to be an instance of whichever command you're currently running.

The hidden power here is that we now have full access to the terminal, so we can show custom messages and provide extra information.

Let's take our $container example and provide some helpful output about what we're actually doing:

class ExternalDataSeeder extends Seeder
{
    public function run(ExternalServiceClient $service)
    {
        $rates = $service->getRates();

        foreach ($rates as $rate) {
            $rate = Rate::create([
                'name' => $rate['name'],
                'multiplier' => $rate['multiplier'],
            ]);

            $this->command->info("Created a new rate with name {$rate->name} and multiplier {$rate->multiplier}.");
        }
    }
}

$this->command->info() will print some text in a happy green colour to the console, providing the user with information about what the seeder is actually doing.

We can take this one step further and use $this->command->ask() to retreive some input from the user. Let's write a MakeUserSeeder class that creates a new User.

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

class MakeUserSeeder extends Seeder
{
    public function run()
    {
        $name = $this->command->ask('Name');
        $email = $this->command->ask('Email');
        $password = $this->command->secret('Password');

        User::create([
            'name' => $name,
            'email' => $email,
            'password' => Hash::make($password),
        ]);

        $this->command->info('User created successfully.');
    }
}

Running this seeder will ask the user to provide a name, email and password. It will then create that User in the database so that you can quickly login without needing to run a separate command or fill in a registration form.

Let's do one more thing. Let's replace our custom MakeUserSeeder with my package ryangjchandler/laravel-make-user.

We can begin by installing the package:

composer require ryangjchandler/laravel-make-user

This package provides a convenient make:user command that will ask similar questions to MakeUserSeeder. You can also extend the command to ask for extra data, more specific to your application.

Inside of our MakeUserSeeder, we can replace the custom logic with a single $this->command->call() statement:

class MakeUserSeeder extends Seeder
{
    public function run()
    {
        $this->command->call('make:user');
    }
}

This will invoke the make:user command inside of the current process, allowing the make:user command to write to the same "output" as the seeder. That means any $this->info() or $this->ask() calls made by the make:user command will appear in the terminal, just like they did in the original MakeUserSeeder.

All in all, I think this is pretty cool. With these 2 things, we can give our seeder classes superpowers and greatly reduce the number of commands or setup steps needed to get a local copy of the application running.

Enjoyed this post or found it useful? Please consider sharing it on Twitter.