feat: seeder
CI / 🧪 Tests Laravel (push) Successful in 2m33s
CI / 🐳 Build & Push Images (push) Successful in 49s

This commit is contained in:
2026-08-14 13:43:35 +02:00
parent 8b60283eeb
commit 238a044d98
3 changed files with 566 additions and 7 deletions
+3 -7
View File
@@ -2,7 +2,6 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
@@ -17,11 +16,8 @@ class DatabaseSeeder extends Seeder
{
$this->call(LegalDocumentSeeder::class);
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
if (app()->environment(['local', 'testing'])) {
$this->call(DemoContentSeeder::class);
}
}
}
+507
View File
@@ -0,0 +1,507 @@
<?php
namespace Database\Seeders;
use App\Enums\DietType;
use App\Enums\FollowStatus;
use App\Enums\IngredientUnit;
use App\Enums\MealPostType;
use App\Enums\MealPostVisibility;
use App\Enums\PacePreference;
use App\Enums\PhysicalActivityLevel;
use App\Enums\UserSex;
use App\Enums\WeightGoal;
use App\Enums\WorkoutSource;
use App\Models\User;
use App\Models\WeightEntry;
use App\Models\WorkoutSessions;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use LogicException;
class DemoContentSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Run the database seeds.
*/
public function run(): void
{
DB::transaction(function (): void {
$users = $this->createUsers();
$this->createMeals($users);
$this->createSocialGraph($users);
$this->createProgressHistory($this->demoUser($users));
});
}
/**
* @return Collection<int, User>
*/
private function createUsers(): Collection
{
$users = collect([
[
'name' => 'Alex Martin',
'email' => 'test@example.com',
'locale' => 'fr',
'bio' => 'Cuisine simple, course à pied et progression régulière.',
'account_verified_at' => now(),
'height' => 178,
'target_weight' => 68,
'weight_goal' => WeightGoal::LOSE_WEIGHT,
'physical_activity_level' => PhysicalActivityLevel::MODERATELY_ACTIVE,
'pace_preference' => PacePreference::NORMAL,
'sex' => UserSex::MAN,
'date_of_birth' => '1994-04-16',
'terms_accepted_at' => now(),
],
[
'name' => 'Camille Green',
'email' => 'camille@bowli.test',
'locale' => 'fr',
'bio' => 'Recettes végétales colorées et faciles à préparer.',
'account_verified_at' => now(),
],
[
'name' => 'Noah Kitchen',
'email' => 'noah@bowli.test',
'locale' => 'en',
'bio' => 'High-protein meals without complicated preparation.',
],
[
'name' => 'Lina Fraîcheur',
'email' => 'lina@bowli.test',
'locale' => 'fr',
'bio' => 'Bols frais, produits de saison et cuisine méditerranéenne.',
'account_verified_at' => now(),
],
[
'name' => 'Sam Fit Food',
'email' => 'sam@bowli.test',
'locale' => 'en',
'bio' => 'Balanced meals for active weeks.',
],
[
'name' => 'Inès Brunch',
'email' => 'ines@bowli.test',
'locale' => 'fr',
'bio' => 'Petits-déjeuners gourmands et brunchs du dimanche.',
],
[
'name' => 'Hugo Maison',
'email' => 'hugo@bowli.test',
'locale' => 'fr',
'bio' => 'Cuisine maison généreuse avec des ingrédients simples.',
],
[
'name' => 'Maya Plant',
'email' => 'maya@bowli.test',
'locale' => 'en',
'bio' => 'Plant-based bowls, soups and snacks.',
'account_verified_at' => now(),
],
[
'name' => 'Louis Endurance',
'email' => 'louis@bowli.test',
'locale' => 'fr',
'bio' => 'Des repas adaptés aux longues sorties et à la récupération.',
],
])->map(
fn (array $profile): User => User::factory()->create($profile)
);
User::factory()->withoutNutritionOnboarding()->create([
'name' => 'Nouveau profil',
'email' => 'onboarding@example.com',
'locale' => 'fr',
'terms_accepted_at' => now(),
]);
return $users;
}
/**
* @param Collection<int, User> $users
*/
private function createMeals(Collection $users): void
{
$templates = collect($this->mealTemplates());
$reviewComments = collect([
'Très bonne idée, je vais essayer cette semaine.',
'Simple, équilibré et vraiment appétissant.',
'Les proportions sont parfaites pour mon déjeuner.',
'Testé hier soir, toute la famille a aimé.',
'Belle assiette et excellente source dinspiration.',
'Parfait après une séance de sport.',
]);
foreach (range(0, 47) as $index) {
$owner = $users->get($index % $users->count());
$template = $templates->get($index % $templates->count());
$eatenAt = now()->subHours(($index * 4) + 1);
if (! $owner instanceof User || ! is_array($template)) {
throw new LogicException('Unable to build the demo meal collection.');
}
$meal = $owner->mealPosts()->create([
'image_url' => $template['imageUrl'],
'caption' => $template['caption'],
'calories' => $template['calories'],
'proteins' => $template['proteins'],
'carbs' => $template['carbs'],
'fats' => $template['fats'],
'title' => $template['title'],
'eaten_at' => $eatenAt,
'visibility' => MealPostVisibility::Public,
'type' => $template['type'],
'ai_generated' => $index % 9 === 0,
'diet_type' => $template['dietType'],
]);
$meal->ingredients()->createMany(
collect($template['ingredients'])
->values()
->map(fn (array $ingredient, int $position): array => [
...$ingredient,
'position' => $position + 1,
])
->all()
);
$eligibleUsers = $users
->reject(fn (User $user): bool => $user->is($owner))
->values();
$engagedUsers = $eligibleUsers
->slice($index % $eligibleUsers->count())
->concat($eligibleUsers->take($index % $eligibleUsers->count()))
->values();
$meal->likedByUsers()->attach(
$engagedUsers->take(2 + ($index % 5))->pluck('id')->all(),
[
'created_at' => $eatenAt->addMinutes(30),
'updated_at' => $eatenAt->addMinutes(30),
]
);
$engagedUsers->take(2 + ($index % 2))->each(
function (User $reviewer, int $reviewerIndex) use ($index, $meal, $reviewComments): void {
$meal->reviews()->create([
'rating' => 5 - (($index + $reviewerIndex) % 2),
'comment' => $reviewComments->get(
($index + $reviewerIndex) % $reviewComments->count()
),
'user_id' => $reviewer->getKey(),
]);
}
);
}
}
/**
* @param Collection<int, User> $users
*/
private function createSocialGraph(Collection $users): void
{
$demoUser = $this->demoUser($users);
$creators = $users
->reject(fn (User $user): bool => $user->is($demoUser))
->values();
$pivot = [
'status' => FollowStatus::Accepted->value,
'created_at' => now()->subDays(10),
'updated_at' => now()->subDays(10),
];
$demoUser->following()->attach($creators->pluck('id')->all(), $pivot);
$creators->take(5)->each(
fn (User $creator) => $creator->following()->attach($demoUser->getKey(), $pivot)
);
$creators->each(function (User $creator, int $index) use ($creators, $pivot): void {
$nextCreator = $creators->get(($index + 1) % $creators->count());
if ($nextCreator instanceof User) {
$creator->following()->attach($nextCreator->getKey(), $pivot);
}
});
}
private function createProgressHistory(User $demoUser): void
{
$demoUser->weightEntries()->delete();
foreach (range(11, 0) as $weeksAgo) {
WeightEntry::factory()->for($demoUser)->create([
'weight_kg' => round(74.8 - ((11 - $weeksAgo) * 0.28), 2),
'measured_at' => now()->startOfDay()->subWeeks($weeksAgo),
]);
}
$demoUser->nutritionPlans()
->where('is_active', true)
->update([
'calories' => 2050,
'proteins' => 130,
'carbs' => 235,
'fats' => 65,
'maintenance_calories' => 2350,
'calorie_adjustment_percent' => -0.1277,
'calculation_details' => [
'activityFactor' => 1.55,
'inputWeightKg' => 71.72,
],
]);
$workouts = collect([
['title' => 'Course facile', 'type' => 'running', 'duration' => 2700, 'calories' => 390, 'distance' => 7200],
['title' => 'Renforcement complet', 'type' => 'strength', 'duration' => 3300, 'calories' => 310, 'distance' => 0],
['title' => 'Sortie vélo', 'type' => 'cycling', 'duration' => 5400, 'calories' => 620, 'distance' => 32000],
['title' => 'Marche active', 'type' => 'walking', 'duration' => 3600, 'calories' => 260, 'distance' => 6100],
]);
foreach (range(0, 11) as $index) {
$workout = $workouts->get($index % $workouts->count());
$source = $index % 4 === 0 ? WorkoutSource::STRAVA : WorkoutSource::MANUAL;
if (! is_array($workout)) {
throw new LogicException('Unable to build the demo workout collection.');
}
WorkoutSessions::factory()->for($demoUser)->create([
'external_id' => $source === WorkoutSource::STRAVA ? "demo-strava-{$index}" : null,
'source' => $source,
'type' => $workout['type'],
'title' => $workout['title'],
'duration_seconds' => $workout['duration'],
'calories_burned' => $workout['calories'],
'distance_meters' => $workout['distance'],
'started_at' => now()->subDays($index)->setTime(18, 30),
]);
}
}
/**
* @param Collection<int, User> $users
*/
private function demoUser(Collection $users): User
{
$demoUser = $users->first(
fn (User $user): bool => $user->email === 'test@example.com'
);
if (! $demoUser instanceof User) {
throw new LogicException('The demo user could not be created.');
}
return $demoUser;
}
/**
* @return list<array<string, mixed>>
*/
private function mealTemplates(): array
{
return [
[
'title' => 'Bowl saumon et avocat',
'caption' => 'Un déjeuner complet avec du riz, des légumes croquants et une sauce citronnée.',
'imageUrl' => 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?auto=format&fit=crop&w=1200&q=85',
'calories' => 620,
'proteins' => 38,
'carbs' => 58,
'fats' => 24,
'type' => MealPostType::LUNCH,
'dietType' => DietType::PESCATARIAN,
'ingredients' => [
['ingredient' => 'Saumon', 'quantity' => 140, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Riz complet', 'quantity' => 160, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Avocat', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
],
],
[
'title' => 'Pancakes aux fruits rouges',
'caption' => 'Des pancakes moelleux accompagnés de fruits rouges et de yaourt.',
'imageUrl' => 'https://images.unsplash.com/photo-1528207776546-365bb710ee93?auto=format&fit=crop&w=1200&q=85',
'calories' => 480,
'proteins' => 24,
'carbs' => 64,
'fats' => 14,
'type' => MealPostType::BREAKFAST,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Flocons davoine', 'quantity' => 80, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Œufs', 'quantity' => 2, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Fruits rouges', 'quantity' => 120, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Poulet rôti et légumes',
'caption' => 'Une assiette familiale, riche en protéines et pleine de couleurs.',
'imageUrl' => 'https://images.unsplash.com/photo-1532550907401-a500c9a57435?auto=format&fit=crop&w=1200&q=85',
'calories' => 590,
'proteins' => 52,
'carbs' => 46,
'fats' => 19,
'type' => MealPostType::DINNER,
'dietType' => DietType::OMNIVORE,
'ingredients' => [
['ingredient' => 'Blanc de poulet', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Pommes de terre', 'quantity' => 220, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Légumes rôtis', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Curry de pois chiches',
'caption' => 'Un curry végétal doux, crémeux et parfait pour le batch cooking.',
'imageUrl' => 'https://images.unsplash.com/photo-1603894584373-5ac82b2ae398?auto=format&fit=crop&w=1200&q=85',
'calories' => 540,
'proteins' => 21,
'carbs' => 72,
'fats' => 18,
'type' => MealPostType::DINNER,
'dietType' => DietType::VEGAN,
'ingredients' => [
['ingredient' => 'Pois chiches', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Lait de coco', 'quantity' => 120, 'unit' => IngredientUnit::MILLILITER],
['ingredient' => 'Épinards', 'quantity' => 100, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Pâtes au pesto maison',
'caption' => 'Un classique rapide avec tomates cerises, roquette et parmesan.',
'imageUrl' => 'https://images.unsplash.com/photo-1473093295043-cdd812d0e601?auto=format&fit=crop&w=1200&q=85',
'calories' => 670,
'proteins' => 25,
'carbs' => 88,
'fats' => 24,
'type' => MealPostType::LUNCH,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Pâtes complètes', 'quantity' => 190, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Pesto', 'quantity' => 2, 'unit' => IngredientUnit::TABLESPOON],
['ingredient' => 'Tomates cerises', 'quantity' => 120, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Toast avocat et œufs',
'caption' => 'Le brunch simple qui tient toute la matinée.',
'imageUrl' => 'https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=1200&q=85',
'calories' => 510,
'proteins' => 26,
'carbs' => 42,
'fats' => 27,
'type' => MealPostType::BRUNCH,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Pain complet', 'quantity' => 2, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Avocat', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Œufs', 'quantity' => 2, 'unit' => IngredientUnit::PIECE],
],
],
[
'title' => 'Tacos de poisson',
'caption' => 'Poisson grillé, chou croquant et sauce au yaourt citronnée.',
'imageUrl' => 'https://images.unsplash.com/photo-1551504734-5ee1c4a1479b?auto=format&fit=crop&w=1200&q=85',
'calories' => 560,
'proteins' => 36,
'carbs' => 61,
'fats' => 20,
'type' => MealPostType::DINNER,
'dietType' => DietType::PESCATARIAN,
'ingredients' => [
['ingredient' => 'Poisson blanc', 'quantity' => 160, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Tortillas', 'quantity' => 3, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Chou rouge', 'quantity' => 100, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Buddha bowl au tofu',
'caption' => 'Tofu croustillant, quinoa, légumes et sauce sésame.',
'imageUrl' => 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?auto=format&fit=crop&w=1200&q=85',
'calories' => 575,
'proteins' => 29,
'carbs' => 69,
'fats' => 22,
'type' => MealPostType::LUNCH,
'dietType' => DietType::VEGAN,
'ingredients' => [
['ingredient' => 'Tofu', 'quantity' => 160, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Quinoa', 'quantity' => 150, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Brocoli', 'quantity' => 140, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Steak et patate douce',
'caption' => 'Une assiette protéinée avec patate douce rôtie et haricots verts.',
'imageUrl' => 'https://images.unsplash.com/photo-1600891964092-4316c288032e?auto=format&fit=crop&w=1200&q=85',
'calories' => 710,
'proteins' => 49,
'carbs' => 65,
'fats' => 28,
'type' => MealPostType::DINNER,
'dietType' => DietType::OMNIVORE,
'ingredients' => [
['ingredient' => 'Steak de bœuf', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Patate douce', 'quantity' => 240, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Haricots verts', 'quantity' => 150, 'unit' => IngredientUnit::GRAM],
],
],
[
'title' => 'Overnight oats banane',
'caption' => 'Préparé la veille avec banane, graines de chia et beurre de cacahuète.',
'imageUrl' => 'https://images.unsplash.com/photo-1517673400267-0251440c45dc?auto=format&fit=crop&w=1200&q=85',
'calories' => 445,
'proteins' => 18,
'carbs' => 62,
'fats' => 15,
'type' => MealPostType::BREAKFAST,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Flocons davoine', 'quantity' => 70, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Banane', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
['ingredient' => 'Graines de chia', 'quantity' => 2, 'unit' => IngredientUnit::TEASPOON],
],
],
[
'title' => 'Salade grecque complète',
'caption' => 'Tomates, concombre, feta, olives et pain pita.',
'imageUrl' => 'https://images.unsplash.com/photo-1540420773420-3366772f4999?auto=format&fit=crop&w=1200&q=85',
'calories' => 495,
'proteins' => 19,
'carbs' => 47,
'fats' => 26,
'type' => MealPostType::LUNCH,
'dietType' => DietType::VEGETARIAN,
'ingredients' => [
['ingredient' => 'Tomates', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Feta', 'quantity' => 80, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Pain pita', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
],
],
[
'title' => 'Ramen au poulet',
'caption' => 'Un bouillon chaud avec nouilles, poulet, œuf et légumes.',
'imageUrl' => 'https://images.unsplash.com/photo-1569718212165-3a8278d5f624?auto=format&fit=crop&w=1200&q=85',
'calories' => 640,
'proteins' => 42,
'carbs' => 78,
'fats' => 18,
'type' => MealPostType::DINNER,
'dietType' => DietType::OMNIVORE,
'ingredients' => [
['ingredient' => 'Nouilles', 'quantity' => 180, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Poulet', 'quantity' => 140, 'unit' => IngredientUnit::GRAM],
['ingredient' => 'Œuf', 'quantity' => 1, 'unit' => IngredientUnit::PIECE],
],
],
];
}
}