Laravel Seeders on Steroids
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...
}
$container
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.
$command
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.