Photo by Vi Tran on Unsplash

Understanding Laravel Seeder Resolution and Execution

Seed and Sow Like a Pro

--

This will be a long article, there is a TL;DR at the end

As a Laravel developer, I’ve recently found myself writing more seeders than ever before.

Thankfully, Laravel’s built-in seeding functionality makes creating, running, and managing seeders a breeze.

It takes care of much of the overhead that we, as developers, would otherwise have to handle.

In this article, we’ll delve into the inner workings of the php artisan db:seed command. We'll explore how it functions step by step, and uncover some hidden functionalities that you might not be aware of.

This deeper understanding will equip you to utilize Laravel’s seeding features more effectively and efficiently, ultimately enhancing your development experience.

Due to the comprehensive nature of the php artisan db:seed command and its intricate handling of various scenarios, we'll break it down step by step.

To begin, let's explore how Laravel efficiently resolves the specific seeder it should execute and gracefully handles situations where an invalid seeder is provided.

    /**
* Get a seeder instance from the container.
*
* @return \Illuminate\Database\Seeder
*/
protected function getSeeder()
{
$class = $this->input->getArgument('class') ??
$this->input->getOption('class');

if (strpos($class, '\\') === false) {
$class = 'Database\\Seeders\\'.$class;
}

if ($class === 'Database\\Seeders\\DatabaseSeeder' &&
! class_exists($class)) {
$class = 'DatabaseSeeder';

}

return $this->laravel->make($class)
->setContainer($this->laravel)
->setCommand($this);
}

This initial step lays the foundation for the entire seeding process, ensuring that Laravel locates and runs the intended seeder flawlessly.

At first, we are looking to find the seeder class:

  • We are looking for the class name in the commands’ argumentsor p options

Looking in the getArgument and getOptions methods, we can see what we receive by calling the command in different ways:

protected function getSeeder()
{
$class = $this->input->getArgument('class') ??
$this->input->getOption('class');

dd(
$this->input->getArgument('class'),
$this->input->getOption('class')
);
. . .

php artisan db:seed OptionClass
-- will output
"OptionClass"
"Database\Seeders\DatabaseSeeder"

. . .
php artisan db:seed --class="ArgumentClass" OR --class=ArgumentClass
-- will output
null
"ArgumentClass"

We can see that getOptions will always return Database\Seeders\DatabaseSeeder , this way, if we call php artisan db:seed it will default to this seeder from where we usually call all our other seeders.

  • If no namespace is provided, we add the default Database\\Seeders\\ namespace

As we have seen previously, whether we call it with options or arguments , the $class variable will not have a namespace, so, naturally, we will go through this step.

The only exception is if we call the command with no arguments or options, in that case, it will have it by default, as we’ve seen above.

if (strpos($class, '\\') === false) {
$class = 'Database\\Seeders\\'.$class;
}

php artisan db:seed --class=DummyArticleClass

-- at the end of execution $class will be
'Database\\Seeders\\DummyArticleClass'
  • If the class matches the default DatabaseSeeder but is not found, it will use the simple DatabaseSeeder class

I am not sure why Laravel does this exactly. But…

I think this check serves as a safety catch to handle various unforeseen situations related to custom seeding structures, missing files, or potential namespace conflicts.

It ensures the command can still function even if the default DatabaseSeeder is unavailable in its typical location.

        if ($class === 'Database\\Seeders\\DatabaseSeeder' &&
! class_exists($class)) {
$class = 'DatabaseSeeder';

}

You can recreate this case by deleting your DatabaseSeeder from its original path and running the php artisan db:seed command. I will not delve into this as it will lead us astray from our main goal.

Now with our seeder class in hand, we will use the Container to create an instance of it, using the make method.

return $this->laravel->make($class)
->setContainer($this->laravel)
->setCommand($this);

If you are curious about the make method, its implementation can be found inside the Illuminate\Container\Container.php class and looks like this:

    /**
* Resolve the given type from the container.
*
* @param string|callable $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}

I have explored how some of the Container’s functionality is implemented in one of my articles

The container and current commandare injected into the seeder for potential use. As of now, we can return our fully resolved and configured seeder.

At the end of all of this, we have retrieved our seeder!

Photo by Ian Talmacs on Unsplash (Our seeder, probably)

We have come far but this was just a little part of the command. Next, I want to explore its constructor and what Resolver it receives. This will help us later on because it will handle our default database connection.

    /**
* Create a new database seed command instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @return void
*/
public function __construct(Resolver $resolver)
{
parent::__construct();

$this->resolver = $resolver;
}

On the ConnectionResolverInterface there are only 3 methods defined:

interface ConnectionResolverInterface
{
/**
* Get a database connection instance.
*
* @param string|null $name
* @return \Illuminate\Database\ConnectionInterface
*/
public function connection($name = null);

/**
* Get the default connection name.
*
* @return string
*/
public function getDefaultConnection();

/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setDefaultConnection($name);
}

The ConnectionResolver, located at Illuminate\Database\, implements the ConnectionResolverInterface. One key aspect of this class is its protected property, $connectionswhich holds a collection of all registered database connections, and the $default which will hold the default database connection.

Now, how does this happen?

Upon instantiation of the ConnectionResolver class, its $connections property is populated to establish a database connection registry.

    /**
* Create a new connection resolver instance.
*
* @param array $connections
* @return void
*/
public function __construct(array $connections = [])
{
foreach ($connections as $name => $connection) {
$this->addConnection($name, $connection);
}
}

/**
* Add a connection to the resolver.
*
* @param string $name
* @param \Illuminate\Database\ConnectionInterface $connection
* @return void
*/
public function addConnection($name, ConnectionInterface $connection)
{
$this->connections[$name] = $connection;
}
  • The constructor allows for an optional array, $connections to be provided, each containing a connection name and its associated configuration details.
  • If connections are supplied during instantiation, the constructor calls the addConnection method for each one, systematically building the registry.

Now, the thing that interests us is the getter and setter for this default connection:

    /**
* Get the default connection name.
*
* @return string
*/
public function getDefaultConnection()
{
return $this->default;
}

/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setDefaultConnection($name)
{
$this->default = $name;
}

They are pretty forward all they do is retrieve or update the property, which we will use when we try to connect to the database:

    /**
* Get a database connection instance.
*
* @param string|null $name
* @return \Illuminate\Database\ConnectionInterface
*/
public function connection($name = null)
{
if (is_null($name)) {
$name = $this->getDefaultConnection();
}

return $this->connections[$name];
}

If no $name argument is provided, the method acts intelligently by first retrieving the currently designated default connection name using getDefaultConnection. This ensures easy access to the primary database connection when no specific name is requested.

If a valid $name argument is supplied, the method directly utilizes it to retrieve the corresponding connection instance from the connections property. This helps developers to work with multiple database connections selectively.

Regardless of the retrieval method (default or named), the method ultimately returns a valid ConnectionInterface instance, representing the requested database connection.

This returned object is then used to execute queries, manage transactions, and perform various other database-related operations within the application.

While we’ve explored essential concepts, the core functionality of handling seeders remains untouched. Let’s delve into that next.

    /**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}

$previousConnection = $this->resolver->getDefaultConnection();

$this->resolver->setDefaultConnection($this->getDatabase());

Model::unguarded(function () {
$this->getSeeder()->__invoke();
});

if ($previousConnection) {
$this->resolver->setDefaultConnection($previousConnection);
}

$this->info('Database seeding completed successfully.');

return 0;
}

This is the bread and butter of our seed command, which will do:

  • Confirmation checks
  • Default connection handling
  • Seed execution
  • Connection restoration
  • Displaying a success message

First, we make sure that you want to run the command:

if (! $this->confirmToProceed()) {
return 1;
}

- - -
Illuminate\Console\ConfirmableTrait.php
---

/**
* Confirm before proceeding with the action.
*
* This method only asks for confirmation in production.
*
* @param string $warning
* @param \Closure|bool|null $callback
* @return bool
*/
public function confirmToProceed($warning = 'Application In Production!', $callback = null)
{
$callback = is_null($callback) ?
$this->getDefaultConfirmCallback()
: $callback;

$shouldConfirm = value($callback);

if ($shouldConfirm) {
if ($this->hasOption('force') && $this->option('force')) {
return true;
}

$this->alert($warning);

$confirmed = $this->confirm('Do you really wish to run this command?');

if (! $confirmed) {
$this->comment('Command Canceled!');

return false;
}
}

return true;
}

Firstly, if no callback is provided, it obtains the default confirmation logic using the getDefaultConfirmationCallback.

$callback = is_null($callback) ? 
$this->getDefaultConfirmCallback()
: $callback;

- - - inside the same class
/**
* Get the default confirmation callback.
*
* @return \Closure
*/
protected function getDefaultConfirmCallback()
{
return function () {
return $this->getLaravel()->environment() === 'production';
};
}

Next, it evaluates the callback logic to check if confirmation is necessary.

$shouldConfirm = value($callback); // true OR false

If confirmation is considered necessary (e.g., in production environments or based on the callback), it immediately returns true, indicating the need to prompt the user.

Now, if the --force option is present and enabled, the method skips confirmation and directly returns true, assuming user intent for forced execution.

if ($this->hasOption('force') && $this->option('force')) {
return true;
}

$this->alert($warning);

$confirmed = $this->confirm('Do you really wish to run this command?');

if (! $confirmed) {
$this->comment('Command Canceled!');

return false;
}

Otherwise, it will alert the user:

Seed in production alert

If the user doesn’t confirm, it outputs a cancellation message using this->comment and returns false, preventing command execution.

And if the user provides confirmation, it implicitly returns true, signaling the caller to proceed with the action.

Our next steps is to configure our connection on which the seeder will run, so we do the following:

$previousConnection = $this->resolver->getDefaultConnection();

$this->resolver->setDefaultConnection($this->getDatabase());

Preserve the Default Connection: The code stores the current default connection using getDefaultConnection. This allows it to potentially switch back after seeding (not sure if you would switch the default connection during a seeder, but it looks like a protection mechanism).

Optional Connection Switch: The code attempts to set a new default connection, using the getDatabase method.

    /**
* Get the name of the database connection to use.
*
* @return string
*/
protected function getDatabase()
{
$database = $this->input->getOption('database');

return $database ?: $this->laravel['config']['database.default'];
}

I do not see this documented anywhere in the seeders' documentation, but you can add a--database option to your command, to use another connection.

Like this:

php artisan db:seed --database=MediumDatabase

$this->getDatabase() -> "MediumDatabase"

The next step involves running our seeder(s), which will first disable our mass assignment protection, just like the documentation states:

Model::unguarded(function () {
$this->getSeeder()->__invoke();
});

Model::unguarded is a method within the Model class that temporarily disables these mass assignment restrictions.

This allows the seeding process to efficiently assign values to various model attributes without encountering protection barriers.

But why do we disable this protection? There are several reasons I can think of:

  • Seeding often involves populating database tables with a significant amount of data, typically programmatically.
  • Assigning values to numerous model attributes can be cumbersome if each attribute needs to be individually accessed and set due to mass assignment protection.
  • By utilizing Model::unguarded, the seeder code can effectively assign data to multiple attributes within the closure, simplifying the process and avoiding the need to bypass protection for each individual attribute.
if ($previousConnection) {
$this->resolver->setDefaultConnection($previousConnection);
}

$this->info('Database seeding completed successfully.');

If $previousConnection exists, it uses the ConnectionResolver to set the default connection back to the previously stored value.

This ensures that the application resumes using the original connection after completing the seeding operation.

To sum it up…

  • Laravel intelligently locates and configures the intended seeder class
  • The command prompts for confirmation before execution, especially in production environments, preventing accidental data manipulation.
  • You can use the --database option to temporarily switch to a different database connection for seeding.
  • Seeding temporarily disables mass assignment protection due to the nature of inserting large amounts of data efficiently.
  • The seeder class, containing your data insertion logic, is executed to populate the database.
  • If the connection was switched, it’s restored to the original one after seeding is complete.

If you found this article helpful and want to see more Laravel deep dives, consider giving it a few claps and following me!

--

--