
Генерування фейкових даних в Laravel
03.07.2018 15:34 | Laravel
У процесі розробки ми часто, а точніше, трохи частіше ніж завжди, використовуємо фейкові дані для тестування функціоналу. Тому пропоную подивитися на прикладах, як генерувати такі дані в 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 користувачів з десятьма постами кожен - так задача вирішується простіше. Підемо в фабрику користувачів і взагалі викинемо з неї поле 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 - тут ще докладніше. Потрібно враховувати, що у нас немає моделі, є тільки таблиця, тобто фабрика нам не підійде. Ну і добре. Ми хочемо, щоб теги прив'язувалися випадковим чином до знову ж таки обраним випадково постами або відео. Для цього нам знову знадобляться масиви з ідентифікаторами (тегів, постів та відео). Далі зробимо так: згенерируємо або 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
, ну а якщо чимало різних зв'язків і потрібно гарненько наповнити базу - користуємося фабриками і пишемо клас-сидер для кожної таблиці. Сподіваюся, цей матеріал стане в нагоді.