Laravel SeedGuard for smart seeding to prevent data duplication

21 May 2025
16 min read
Laravel SeedGuard for smart seeding to prevent data duplication
00:0000:00

The Problem

As a Laravel developer, you’ve likely spent a lot of time working with database seeders. You create a new seeder file, seed the database, and sometimes wipe it entirely with migrate:fresh to start over. You might seed a specific class using the --class option, but manually specifying the class name feels repetitive and time-consuming. Worse, if you forget to include the --class option, Laravel re-seeds everything from scratch, potentially filling your database with duplicate data.

The Inspiration

Laravel’s migration system provides a clever solution to a similar issue. It avoids re-running migration files by tracking them in a dedicated migrations table. This ensures that only new migrations are executed, preventing duplication and saving time. Why not apply this same tracking concept to database seeders?

The Solution

By leveraging the migration system’s tracking mechanism, we can streamline the seeding process. The idea is simple: create a parent class called SeedGuard for all seeders that mimics the migration tracking logic. The SeedGuard class will:

  1. Create a seeders table to track executed seeders.
  2. Use database transactions to ensure a seeder’s entry is only recorded if the seeding process succeeds, automatically rolling back if it fails.
  3. Check if a seeder has already been executed before running it, skipping it if it’s already in the seeders table.

Here’s the full SeedGuard class you can utilize:

<?php /** @noinspection PhpUnhandledExceptionInspection */

namespace App\Support\Libraries\SeedGuard;
use Exception;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Psr\Log\LoggerInterface;

/**
 * SeedGuard is a custom seeder that extends Laravel's Seeder class, adding tracking and error handling functionality.
 * It ensures seeders are only executed once by logging them in a 'seeders' table. If any errors occur during seeding,
 * they are logged for review.
 */
class SeedGuard extends Seeder
{
    /**
     * @var bool $tableEnsuredExists Tracks if the 'seeders' table existence check has been performed to avoid redundant checks.
     */
    private bool $tableEnsuredExists = false;

    /**
     * Ensures the existence of the 'seeders' table. If the table doesn't exist, it is created to track seeder executions.
     * This method runs only once per instance of SeedGuard.
     *
     * @return void
     */
    private function ensureTableExists(): void
    {
        if($this->tableEnsuredExists){
            return;
        }
        if (!Schema::hasTable('seeders')) {
            Schema::create('seeders', function (Blueprint $table) {
                $table->id();
                $table->string('seeder', 190)->unique();
                $table->timestamp('seeded_at')->useCurrent();
            });
        }
        $this->tableEnsuredExists = true;
    }

    /**
     * Configures and returns a logger instance for logging errors.
     *
     * @return LoggerInterface Returns a PSR-compliant logger set to write error logs with daily rotation.
     */
    private function getLogger(): LoggerInterface
    {
        return Log::build([
            'driver' => 'daily',
            'path' => storage_path('logs/seeder-errors.log'),
            'level' => 'error',
            'days' => 7,
        ]);
    }

    /**
     * Checks if a seeder has already been executed.
     *
     * This method queries the `seeders` table to determine if a specific seeder
     * has been run previously. If the seeder exists in the table, it has already
     * been executed, and the method returns true; otherwise, it returns false.
     *
     * @param string $class The name of the seeder class to check.
     * @return bool Returns true if the seeder was already seeded; false otherwise.
     */
    protected function wasSeeded($class): bool
    {
        return DB::table('seeders')->where('seeder', $class)->exists();
    }

    /**
     * Enhanced call method to execute a seeder with tracking and error handling.
     * This method ensures that the seeders table exists, runs the specified seeder
     * within a transaction, logs any errors that occur, and records the successful
     * execution in the seeders table.
     *
     * @param string $class The seeder class to run.
     * @param bool $silent Whether to suppress console output during seeding.
     * @param array $parameters Additional parameters to pass to the seeder.
     * @return bool Returns true if the seeder was executed and logged successfully.
     * @throws \Throwable Throws any exception encountered during seeder execution.
     */
    public function call($class, $silent = false, array $parameters = []): bool
    {
        $this->ensureTableExists();

        if ($this->wasSeeded($class)) {
            $this->command->getOutput()->writeln("<info>Skipped (already seeded):</info>  {$class}");
            return true;
        }

        $result = false;
        DB::transaction(function () use(&$result, $class, $silent, $parameters)  {
            try{
                parent::call($class, $silent, $parameters);
            }catch (Exception $e){
                $this->getLogger()->error($e);
                throw $e;
            }
            DB::table('seeders')->insert(['seeder' => $class]);
            $result = true;
        });

        return $result;
    }

    public function __invoke(array $parameters = [])
    {
        // in Laravel 11+, its calling __invoke() not call()
        $callback = function(){}; //no-op
        
        $this->ensureTableExists();

        $class = static::class;
        if ($this->wasSeeded($class)) {
            $this->command->getOutput()->writeln("<info>Skipped (already seeded):</info>  {$class}");
            return $class;
        }
        
        DB::transaction(function () use(&$callback, $class, $parameters)  {
            try{
                $callback = parent::__invoke($parameters);
            }catch (Exception $e){
                $this->getLogger()->error($e);
                throw $e;
            }
            DB::table('seeders')->insert(['seeder' => $class]);
        });

        return $callback;
    }
}

To make this even more seamless, instead of manually extending SeedGuard in every seeder, you can create a custom stub that Laravel will use when generating new seeder files. Simply create a file at stubs/seeder.stub with the following content:

<?php

namespace Database\Seeders;

use App\Support\Libraries\SeedGuard\SeedGuard;

class {{ class }} extends SeedGuard
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(MySeeder::class);
    }
}

Why This Matters

The SeedGuard approach is a game-changer for Laravel developers working on projects with frequent seeding needs. Whether you’re setting up test data, populating a development environment, or managing seeders in a team setting, SeedGuard saves time by preventing duplicate seeding and eliminating the need to manually specify the --class option. The transaction-based logic ensures data integrity, rolling back changes if a seeder fails, which is especially valuable in complex applications with interdependent seeders.

Stay Updated with the Latest SeedGuard

To keep SeedGuard evolving with new features and improvements, I maintain the latest version of the class in a dedicated Gist. As I make updates—whether it’s adding new functionality, optimizing performance, or addressing edge cases—the Gist will always reflect the most current implementation. Be sure to check the Gist regularly for the latest version of SeedGuard to ensure you’re using the most up-to-date code in your projects. You can find it here: SeedGuard Gist.

Conclusion

The SeedGuard class brings the elegance of Laravel’s migration system to database seeding, making the process more efficient, reliable, and developer-friendly. By tracking executed seeders and leveraging database transactions, SeedGuard eliminates common pain points like duplicate data and manual class specification. With the custom stub, integrating SeedGuard into your workflow is seamless, allowing you to focus on building features rather than managing seeders. Adopt SeedGuard in your next Laravel project to streamline your seeding process and boost productivity. Happy coding!

Ibraheem G. Al-Nabriss

Ibraheem G. Al-Nabriss

Senior Full Stack Web Developer with expertise in building SaaS and modern web applications. Specialized in PHP/Laravel and React/Vue ecosystems

Share this post

Related Posts