Генерирование фейковых данных в Laravel

Генерирование фейковых данных в Laravel

В процессе разработки мы зачастую, а точнее, чуть чаще чем всегда, используем фейковые данные для тестирования функционала. Посему предлагаю посмотреть на примерах, как генерировать данные c различными связями в Laravel 5.6 с помощью Faker.

Задача 1.

Самая простая - сгенерировать N пользователей.

Решение

Для таких тривиальных вещей проще и дешевле использовать tinker. Поскольку фабрика для создания пользователей в Laravel есть изначально, даём команду в консоли:

php artisan tinker

 

 ...И создаём, скажем, десяток пользователей:

>>> factory(App\User::class, 10)->create()

Вопрос решён.

 

Задача 2

Немного усложним: залить в базу N постов, каждый из которых привязан к пользователю. Предположим, что миграция постов выглядит следующим образом:

Schema::create('posts', function (Blueprint $table) {
      $table->increments('id');
      $table->integer('user_id')->unsigned();
      $table->string('title');
      $table->text('body');
      $table->boolean('live')->default(false);
      $table->timestamps();

      $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
 
Решение

Для начала реализуем фабрику для постов. Команда в консоли:

php artisan make:factory PostFactory --model=Post

 

После чего идём в файл фабрики (project/database/factories/PostFactory.php) и дописываем в нём следующее:

$factory->define(App\Post::class, function (Faker $faker) {
    return [
        'user_id' => function() {
            return factory(App\User::class)->create()->id;
        },
        'title' => $faker->sentence(5),
        'body' => $faker->text(300),
        'live' => $faker->boolean
    ];
});

 

То есть, при генерации поста мы в колбэке будем сразу же генерировать пользователя и "дёргать" его id для поля user_id. Снова вызываем tinker и на сей раз просим создать десяток постов:

>>> factory(App\Post::class, 10)->create()

 

И, кстати, поскольку выше нигде не упоминаются отношения пользователь - посты, код сработает, даже если Вы забыли определить отношения между данными сущностями в моделях. Но, как говорится - то такое. Мы же идём дальше. Полагаю, не стоит объяснять, что ситуация, когда у каждого пользователя всего один пост, в реальной жизни маловероятна, потому не будем столь академичны.

 

Задача 3

Создать по 10 постов на каждого из 5 создаваемых пользователей. Таблица пользователей идентична таблице из предыдущего примера.

Решение

Вот не зря так поставил условие - может показаться, что следует сгенерировать 10 постов и каким-то образом "всунуть" им нужный user_id. Но в данном случае следует подумать и перевернуть условие: сгенерировать 5 пользователей с 10-ю постами каждый - так задача решается проще. Пойдём в фабрику пользователей и вообще выброcим из неё поле user_id:

$factory->define(App\Post::class, function (Faker $faker) {
    return [
        'title' => $faker->sentence(5),
        'body' => $faker->text(300),
        'live' => $faker->boolean
    ];
});

 

На сей раз не будем использовать tinker, доберёмся наконец до seeder-а. Открываем DatabaseSeeder (project/database/seeds/DatabaseSeeder.php) и добавляем в метод run() всего пару строк:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        factory(App\User::class, 5)->create()->each(function($user) {
            $user->posts()->saveMany(factory(App\Post::class, 10)->make());
        });
    }
}

 

Поясню, что происходит: после создания пользователей, мы "проходимся" по каждому из них и создаём по десятку постов. Обратим внимание:

  • мы должны исользовать не метод save(), а saveMany(), поскольку передаём не один пост, а коллекцию постов
  • чтобы получить эту самую коллекцию постов нам нужен метод make(). В отличие от create() он не сохраняет экземпляр в базе, а лишь создаёт его

Кроме того, мы исходили из соображений, что отношения "один ко многим" определены, то бишь для того, чтобы всё это счастье заработало, следует в модели пользователя прописать метод posts():

...
use App\Post;

class User extends Authenticatable
{
    ...

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

 

Вот теперь смело идём в консоль и говорим:

php artisan db:seed

 

Результат - 5 юзеров и 50 постов - по десятку на человека.

 

Задача 4

Давайте добавим к постам категории. Другими словами, требуется сгенерировать посты и привязать к каждому посту какого-либо пользователя и категорию. Миграция категорий:

Schema::create('categories', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->string('slug');
    $table->boolean('live')->default(false);
    $table->timestamps();
});

 

Миграция постов теперь такова:

Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('user_id')->unsigned();
    $table->integer('category_id')->unsigned()->nullable();
    $table->string('title');
    $table->text('body');
    $table->boolean('live')->default(false);
    $table->timestamps();

    $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
    $table->foreign('category_id')->references('id')->on('categories');
});

 

Решение

Сообразим фабрику категорий:

php artisan make:factory CategoryFactory --model=Category

 

И отредактируем соответствующий файл (project/database/factories/CategoryFactory.php):

<?php

use Faker\Generator as Faker;

$factory->define(App\Category::class, function (Faker $faker) {
    $title = $faker->sentence(2);

    return [
        'title' => $title,
        'slug' => str_slug($title),
        'live' => $faker->boolean,
        'sort_order' => rand(1, 50)
    ];
});

 

Теперь снова подумаем. Во-первых, и здесь хочется и можно обойтись малой кровью - без отдельных классов-сидеров. Во-вторых, есть два варианта решения:

  • снова сгенерировать по 5 юзеров, каждый с 10 постами, но при этом не забыть вставить id категории в каждый пост
  • генерировать посты, каждый из которых связывать с каким-то случайным пользователем и случайной категорией 

Я не противоречу тому, что говорил в задаче 3, т.е. перевернуть условие и т.д., фактически я говорю о том, что в некоторых ситуациях одно решение выглядит предпочтительнее другого. В данном случае, рассмотрим оба. Итак, первое. По сути мы повторим то, что было в предыдущем варианте, но дополним его ещё одним колбэком, в котором в модельку поста будем вставлять id случайной категории. Разумеется, предварительно нужно их создать. Всю работу снова сделаем в DatabaseSeeder и выглядеть метод run() будет так:

public function run()
{
    $categoryIds = factory(App\Category::class, 5)->create()->pluck('id')->toArray();

    factory(App\User::class, 5)->create()->each(function($user) use ($categoryIds) {
        $user->posts()->saveMany(
            factory(App\Post::class, 10)->make()->each(function($post) use ($categoryIds) {
                $post->category_id = rand(1, count($categoryIds)
            );
        }));
    });
}

 

Необходимость указывать use ($categoryIds) в колбэках немного напрягает, но PHP это не JavaScript, посему приходится мирится. Объяснил всё выше, но повторюсь - первым делом мы создали категории, и не отходя от кассы вытащили их id в массив. Затем после создания пользователей, перебираем их, генерируя для каждого по 10 постов. При этом посты не сохраняем в базу данных, а просто генерируем. В свою очередь, после того как модели постов готовы, перебираем их и присваиваем свойству category_id случайное значение из массива идентификаторов категорий. Возьмём на заметку, что по факту имеем вложенный цикл, то бишь сложность алгоритма N^2.

Теперь перейдём ко второму варианту. На сей раз мы предварительно создадим и пользователей, и категории, также снова "дёрнем" их id в соответствующие массивы. Затем создадим модели постов, перебирая их, вставим в user_id и category_id случайные значения из массивов $userIds и $categoryId. И ещё один важный момент - мы не хотим обращаться к базе данных при создании каждого поста - не кошерно это. Посему коллекцию постов превратим в массив и отправим всё одним запросом. Вот полный код класса:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $userIds = factory(App\User::class, 3)->create()->pluck('id')->toArray();
        $categoryIds = factory(App\Category::class, 5)->create()->pluck('id')->toArray();

        $posts = factory(App\Post::class, 25)->make()->each(function($post) use ($userIds, $categoryIds) {
            $post->user_id = array_random($userIds);
            $post->category_id = array_random($categoryIds);
            // $post->save();
        })->toArray();

        App\Post::insert($posts);
    }
}

 

Результат - всё тех же 50 постов. Но на этот раз никакого коммунизма - у кого-то больше постов, у кого-то меньше, что выглядит гораздо естественней. Что приятно - вложенного цикла больше нет, т.е. сложность алгоритма N, а это значит, что второй вариант будет работать быстрее. Как бы там ни было, выбраем версию, которая нам больше нравится и выполняем команду:

php artisan db:seed

 

Задача 5

Последнюю задачу возьмём посерьёзнее: создать пользователей, категории и посты - связи как и прежде один ко многим (пользователь - посты, категория - посты); создать ещё какой-то контент, например, видео; создать тэги, связь с постами и видео - полиморфная, многие ко многим.

Решение

Решать также будем несколько иначе - для каждой сущности, а также для промежуточной таблицы taggables будем создавать свой класс-seeder. С видео и тэгами особо заморачиваться не будем, вот методы up() из соответствующих миграций. Видео:

public function up()
{
    Schema::create('videos', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->string('url');
        $table->timestamps();
    });
}

 

Тэги:

public function up()
{
    Schema::create('tags', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
    });
}

 

Также нам понадобится промежуточная таблица taggables. Команда для миграции:

php artisan make:migration create_taggables_table --create=taggables

 

И метод up() данной миграции:

public function up()
{
    Schema::create('taggables', function (Blueprint $table) {
        $table->integer('tag_id')->unsigned();
        $table->integer('taggable_id')->unsigned();
        $table->string('taggable_type');

        $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
    });
}

 

Далее создадим фабрики:

php artisan make:factory VideoFactory --model=Video
...
php artisan make:factory TagFactory --model=Tag

 

И отредактируем их. Видео:

<?php

use Faker\Generator as Faker;

$factory->define(App\Video::class, function (Faker $faker) {
    return [
        'title' => $faker->sentence(4),
        'url' => $faker->url
    ];
});

 

...и тэги:

<?php

use Faker\Generator as Faker;

$factory->define(App\Tag::class, function (Faker $faker) {
    return [
        'name' => $faker->word
    ];
});

 

Следующий шаг - классы сидеры, в том числе и для taggables. Все команды в консоли аналогичны, поэтому приведу команду только для сидера пользователей:

php artisan make:seeder UsersTableSeeder

 

На простых классах seeder-ах останавливаться не буду, а лишь приведу их код. 

UsersTableSeeder:

<?php

use App\User;
use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
    public function run()
    {
        factory(User::class, 5)->create();
    }
}

 

CategoriesTableSeeder:

<?php

use App\Category;
use Illuminate\Database\Seeder;

class CategoriesTableSeeder extends Seeder
{
    public function run()
    {
        factory(Category::class, 5)->create();
    }
}

 

VideosTableSeeder:

<?php

use App\Video;
use Illuminate\Database\Seeder;

class VideosTableSeeder extends Seeder
{
    public function run()
    {
        factory(Video::class, 20)->create();
    }
}

 

TagsTableSeeder:

<?php

use App\Tag;
use Illuminate\Database\Seeder;

class TagsTableSeeder extends Seeder
{
    public function run()
    {
        factory(Tag::class, 10)->create();
    }
}

 

PostsTableSeeder - фактически мы делаем то же, что и раньше. Отличие в том, что теперь не нужно создавать пользователей и категории - за это отвечают соответствующие классы. Но нам всё так же нужны их массивы id. После мы снова сгенерируем модели постов, и в поля user_id, category_id вставим случайные значения из вышеупомянутых массивов. Чтобы не напрягать базу данных кучей запросов, конвертируем наши модели в массивы и запишем в БД всё одним махом, воспользовавшись методом insert():

<?php

use App\{Category, Post, User};
use Illuminate\Database\Seeder;

class PostsTableSeeder extends Seeder
{
    public function run()
    {
        $userIds = User::pluck('id')->toArray();
        $categoryIds = Category::pluck('id')->toArray();

        $posts = factory(Post::class, 20)->make()->each(function($post) use($userIds, $categoryIds) {
            $post->user_id = array_random($userIds);
            $post->category_id = array_random($categoryIds);
        })->toArray();

        Post::insert($posts);
    }
}

 

TaggablesTableSeeder - тут ещё подробнее. Нужно учитывать, что у нас нет модели, есть только таблица, т.е. фабрика нам не подойдёт. Ну и ладно. Мы хотим, чтобы тэги привязывались случайным образом к опять-таки случайно выбранным записям постов или видео. Для чего нам снова понадобятся массивы с id (тэгов, постов и видео). Далее поступим так: сгенерируем либо 0, либо 1. Если будет единица - привяжем тэг к какому-то посту, если ноль - к какому-нибудь видео. После чего, запишем массив в базу (сформируем 30 записей):

<?php

use App\{Post, Tag, Video};
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class TaggablesTableSeeder extends Seeder
{
    public function run()
    {
        $tagIds = Tag::pluck('id')->toArray();
        $postIds = Post::pluck('id')->toArray();
        $videoIds = Video::pluck('id')->toArray();

        $taggables = [];
        for ($i = 0; $i < 30; $i++) {
            $taggable['tag_id'] = array_random($tagIds);

            $random = rand(0, 1);
            if ($random) {
                $taggable['taggable_id'] = array_random($postIds);
                $taggable['taggable_type'] = Post::class;
            } else {
                $taggable['taggable_id'] = array_random($videoIds);
                $taggable['taggable_type'] = Video::class;
            }

            $taggables[] = $taggable;
        }

        DB::table('taggables')->insert($taggables);
    }
}

 

И последняя команда:

php artisan db:seed

 

Вот и всё - эта задача также решена. 

Заключение

Может показаться, что работы немало, но на самом деле написание всего этого кода целиком и полностью себя оправдывает, поскольку существенно экономит время в дальнейшем. Ещё раз повторим: для чего-то простого используем фабрики и tinker, для вещей чуть посложнее можно обойтись фабриками и одним классом DatabaseSeeder, ну а если немало различных связей и нужно хорошенько наполнить базу - пользуемся фабриками и пишем класс-сидер для каждой таблицы. Надеюсь материал будет полезен.