Advance interactive database seeding in Laravel

We all need Database seeding when developing medium to large size application or any API, Laravel comes with Database Seeding capabilities but since Laravel 5.1, framework has introduced the Model Factory which is amazing, you just define your Model default fields values using Faker, which gives you all type of dummy data, I find its lot faster and very easy to use.

Here is what I do to seed my Laravel applications, I find it very effective in my development process because I see myself trashing database and re-seeding quite often. So what I did is to fuse command and seeder together, it gives flexibility and customization when seeding, let me show you what I mean.

We are going to seed a simple youtube like video sharing application for this demonstration. It will have User, Channel, Video & Comments Models.

Add a database and configure the credential, we will create the migration first.

I am using Laravel 5.3, but anything 5.1 or above should work

Laravel Model & Migration

Laravel comes with user migration out of the box so we are going to start with Channel.

php artisan make:model Channel -m

This will make Channel model and migration since we passed -m option. Do the same for Video and Comment model.

Define Model Factory

Laravel allows you to define a default set of attributes for each of your Eloquent models using model factories. To get started, take a look at the database/factories/ModelFactory.php file in your application.

Let’s setup basic factories for all of our app models.

$factory->define(App\User::class, function (Faker\Generator $faker) {
    static $password;
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'avatar' => 'https://randomuser.me/api/portraits/' . $faker->randomElement(['men', 'women']) . 'men/' . rand(1,99) . '.jpg',
        'password' => $password ?: $password = bcrypt('secret'),
        'remember_token' => str_random(10),
    ];
});

$factory->define(App\Channel::class, function(\Faker\Generator $faker) {
    return [
        'name' => $faker->company,
        'logo' => $faker->imageUrl(60, 60),
        'cover' => $faker->imageUrl(),
        'about' => $faker->text(rand(100, 500)),
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        }
    ];
});

$factory->define(App\Video::class, function(\Faker\Generator $faker) {
    return [
        'title' => $faker->text(),
        'description' => $faker->realText(rand(80, 600)),
        'published' => $faker->boolean(),
        'url' => $faker->url,
        'thumbnail' => $faker->imageUrl(640, 480, null, true),
        'allow_comments' => $faker->boolean(80),
        'views' => rand(0, 872323)
    ];
});

$factory->define(App\Comment::class, function(\Faker\Generator $faker) {
    return [
        'body' => $faker->realText(rand(10, 300)),
        'video_id' => function () {
            // Get random video id
            return App\Video::inRandomOrder()->first()->id;
        },
        'user_id' => function () {
            // Get random user id
            return App\User::inRandomOrder()->first()->id;
        }
    ];
});

You can see the database scheme for all models, update the migration files to mimic same structure.

Database Seeder

Now we are ready to start developing our seeder, I am not going to create seeder classes separately but going to put all of the logic in database/seeds/DatabaseSeeder.php.

Since our seeder class has Command Line reference we can use most of the options a Laravel command offers, you can check the docs for Laravel Commands here.

Our database seeder is not going to run everything automatically, it will be interactive and will be prompting a number of questions, like number of users you need, and how many videos, and comments you need as test data. open the DatabaseSeeder.php and update this.

/**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // Un Guard model
        Model::unguard();

        // Ask for db migration refresh, default is no
        if ($this->command->confirm('Do you wish to refresh migration before seeding, it will clear all old data ?')) {

            // Call the php artisan migrate:refresh using Artisan
            $this->command->call('migrate:refresh');

            $this->command->line("Data cleared, starting from blank database.");
        }

        // How many users you need, defaulting to 20
        $numberOfUser = $this->command->ask('How many users do you need ?', 20);

        $this->command->info("Creating {$numberOfUser} users, each will have a channel associated.");

        // Create the channel, it will create a user and assign the channel
        $channels = factory(App\Channel::class, $numberOfUser)->create();

        $this->command->info('Users Created!');

        // How many videos per channel
        $videoRange = $this->command->ask('How many videos per channel should have, give a range ?', '10-20');

        // Loop and create the video in range with channel id
        $channels->each(function($channel) use ($videoRange){
            factory(App\Video::class, $this->getRandomRange($videoRange))
                    ->create(['channel_id' => $channel->id]);
        });

        $this->command->info("Now all Channels have {$videoRange} videos !");

        // Now how many comments per video
        $commentRange = $this->command->ask('Give a range for comments per video ?', '0-20');

        // Get all video and give each one some comment in asked range
        \App\Video::all()->each(function() use ($commentRange) {
            factory(App\Comment::class, $this->getRandomRange($commentRange))->create();
        });

        $this->command->info("{$commentRange} Comment(s) added for videos !");

        $this->command->info("Hurrah! Database has been seeded.");

        // Re Guard model
        Model::reguard();
    }

    /**
     * Return random value in given range
     *
     * @param $videoRange
     * @return int
     */
    function getRandomRange($videoRange)
    {
        return rand(...explode('-', $videoRange));
    }

All of the code is well commented, so you can understand what’s going on, now let’s test it. If you run php artisan db:seed it will ask following questions and seed the data accordingly.

In `getRandomRange` method I have used ... (splat operator), Arrays and Traversable objects can be unpacked into argument lists when calling functions, it was added PHP 5.6.

  1. Do you wish to refresh migration before seeding, it will clear all old data ? (yes/no) [no]:
  2. How many users do you need ? [20]:
  3. How many videos per channel should have, give a range ? [10-20]:
  4. Give a range for comments per video ? [0-20]:

I hope you liked it, now our seeder is interactive, you can generate test data in with flexibility, it’s not limited to this, you can add any laravel command and make the seeder even more flexible, please comment, & one more thing, I will be converting this into an API and after that in Vue.js app in future, follow me on twitter for updates.

2 Responses to “Advance interactive database seeding in Laravel”

  1. Jesus Baron

    This is awesome! It makes seeding much more intelligent and enjoyable 🙂
    Question: I’m working on an enterprise app that needs a lot of seeders (more than 20 tables). It’s looks kinda ugly to throw ’em all on the root database/seeder. Is there a recommended way, good practice to organize all the seeders in a more scalable way?

    • Glad you find it useful, certainly there is a way to organize your seeder in a bigger app, you will need to create separate seeder class. Here is how I have done it in my app. I use classic php artisan make:seeder BaseDataSeeder command to create seeder.

      In BaseDataSeeder goes all the data related to bootstrapping the app, like countries, roles, settings etc. and other seeder class for UserSeeder and MediaSeeder etc. In main DatabaseSeeder I just call the appropriate seeder whenever I want.


      public function run()
      {
      Model::unguard();

      // disable fk constrain check
      DB::statement('SET FOREIGN_KEY_CHECKS=0;');

      ...
      $this->call('BaseDataSeeder');
      ...
      $this->call('UserSeeder');

      // enable it back
      DB::statement('SET FOREIGN_KEY_CHECKS=1;');