Skip to content

Commit f63fd30

Browse files
authored
perf: optimizing the product seeder (CT-84)
1 parent 2bab5e1 commit f63fd30

2 files changed

Lines changed: 179 additions & 85 deletions

File tree

database/factories/ProductFactory.php

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
namespace Eclipse\Catalogue\Factories;
44

55
use Eclipse\Catalogue\Models\Product;
6-
use Exception;
76
use Illuminate\Database\Eloquent\Factories\Factory;
87
use Illuminate\Support\Carbon;
9-
use Illuminate\Support\Facades\Log;
108

119
class ProductFactory extends Factory
1210
{
@@ -47,21 +45,17 @@ public function definition(): array
4745
];
4846
}
4947

50-
public function configure(): static
48+
public function withImages(): static
5149
{
5250
return $this->afterCreating(function (Product $product) {
5351
$imageNumber = rand(1, 15);
5452
$imagePath = storage_path("app/public/sample-products/{$imageNumber}.jpg");
5553

5654
if (file_exists($imagePath)) {
57-
try {
58-
$product->addMedia($imagePath)
59-
->preservingOriginal()
60-
->withCustomProperties(['is_cover' => true])
61-
->toMediaCollection('images');
62-
} catch (Exception $e) {
63-
Log::warning("Failed to attach image to product {$product->id}: ".$e->getMessage());
64-
}
55+
$product->addMedia($imagePath)
56+
->preservingOriginal()
57+
->withCustomProperties(['is_cover' => true])
58+
->toMediaCollection('images');
6559
}
6660
});
6761
}

database/seeders/ProductSeeder.php

Lines changed: 174 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use Eclipse\Catalogue\Models\ProductType;
1111
use Exception;
1212
use Illuminate\Database\Seeder;
13-
use Illuminate\Support\Facades\Http;
13+
use Illuminate\Support\Facades\DB;
1414
use Illuminate\Support\Facades\Storage;
1515

1616
class ProductSeeder extends Seeder
@@ -25,7 +25,7 @@ public function run(): void
2525
$productTypes = ProductType::all();
2626

2727
Product::factory()
28-
->count(100)
28+
->count(20)
2929
->create([
3030
'product_type_id' => function () use ($productTypes) {
3131
return $productTypes->random()->id;
@@ -35,112 +35,200 @@ public function run(): void
3535
$tenantFK = config('eclipse-catalogue.tenancy.foreign_key');
3636
$tenantModel = config('eclipse-catalogue.tenancy.model');
3737

38-
$products = Product::query()->latest('id')->take(100)->get();
38+
$products = Product::query()->latest('id')->take(20)->get();
39+
$groupProductInserts = [];
40+
$productDataInserts = [];
3941

40-
foreach ($products as $index => $product) {
41-
if ($tenantFK && $tenantModel && class_exists($tenantModel)) {
42-
$tenants = $tenantModel::all();
42+
if ($tenantFK && $tenantModel && class_exists($tenantModel)) {
43+
$tenants = $tenantModel::all();
44+
45+
$categoriesByTenant = Category::query()
46+
->withoutGlobalScopes()
47+
->get()
48+
->groupBy($tenantFK);
49+
50+
$groupsByTenant = Group::all()->groupBy($tenantFK);
51+
52+
foreach ($products as $index => $product) {
4353
foreach ($tenants as $tenant) {
44-
$categoryId = Category::query()
45-
->withoutGlobalScopes()
46-
->where($tenantFK, $tenant->id)
47-
->inRandomOrder()
48-
->value('id');
54+
$categories = $categoriesByTenant->get($tenant->id);
55+
$categoryId = $categories?->random()?->id;
4956

50-
$productData = ProductData::factory()->create([
57+
$productDataInserts[] = [
5158
'product_id' => $product->id,
5259
$tenantFK => $tenant->id,
5360
'is_active' => true,
5461
'has_free_delivery' => false,
5562
'category_id' => $categoryId,
56-
]);
63+
];
5764

58-
// Assign random product status for this tenant
59-
$this->assignRandomProductStatus($productData, $tenant->id);
60-
61-
// Get groups for this specific tenant
62-
$tenantGroups = Group::where($tenantFK, $tenant->id)->get();
65+
$tenantGroups = $groupsByTenant->get($tenant->id, collect());
6366
$groupsToAdd = $this->determineGroupsForProduct($index, $tenantGroups);
6467

65-
foreach ($groupsToAdd as $group) {
66-
$group->addProduct($product);
68+
foreach ($groupsToAdd as $groupIndex => $group) {
69+
$groupProductInserts[] = [
70+
'group_id' => $group->id,
71+
'product_id' => $product->id,
72+
'sort' => $groupIndex + 1,
73+
];
6774
}
6875
}
69-
} else {
70-
$categoryId = Category::query()->inRandomOrder()->value('id');
76+
}
77+
} else {
78+
$categories = Category::all();
79+
$groups = Group::all();
7180

72-
$productData = ProductData::factory()->create([
81+
foreach ($products as $index => $product) {
82+
$productDataInserts[] = [
7383
'product_id' => $product->id,
7484
'is_active' => true,
7585
'has_free_delivery' => false,
76-
'category_id' => $categoryId,
77-
]);
78-
79-
// Assign random product status for non-tenant scenario
80-
$this->assignRandomProductStatus($productData, null);
86+
'category_id' => $categories->random()->id,
87+
];
8188

82-
// For non-tenant scenarios, use all groups
83-
$groups = Group::all();
8489
$groupsToAdd = $this->determineGroupsForProduct($index, $groups);
85-
foreach ($groupsToAdd as $group) {
86-
$group->addProduct($product);
90+
foreach ($groupsToAdd as $groupIndex => $group) {
91+
$groupProductInserts[] = [
92+
'group_id' => $group->id,
93+
'product_id' => $product->id,
94+
'sort' => $groupIndex + 1,
95+
];
8796
}
8897
}
8998
}
99+
100+
if (! empty($productDataInserts)) {
101+
ProductData::insert($productDataInserts);
102+
103+
$createdProductData = ProductData::whereIn('product_id', $products->pluck('id'))->get();
104+
105+
if ($tenantFK && $tenantModel && class_exists($tenantModel)) {
106+
foreach ($createdProductData as $productData) {
107+
$this->assignRandomProductStatus($productData, $productData->{$tenantFK});
108+
}
109+
} else {
110+
foreach ($createdProductData as $productData) {
111+
$this->assignRandomProductStatus($productData, null);
112+
}
113+
}
114+
}
115+
116+
if (! empty($groupProductInserts)) {
117+
DB::table('pim_group_has_product')->insert($groupProductInserts);
118+
}
119+
120+
$this->attachImagesToProducts($products);
90121
}
91122

92123
private function determineGroupsForProduct(int $productIndex, $groups): array
93124
{
94-
$groupsToAdd = [];
125+
if ($groups->isEmpty()) {
126+
return [];
127+
}
95128

96-
// Randomly assign 1-3 groups per product
97129
$numGroupsToAdd = rand(1, min(3, $groups->count()));
98130

99-
// Get random groups for this product
100-
$randomGroups = $groups->random($numGroupsToAdd);
101-
102-
foreach ($randomGroups as $group) {
103-
$groupsToAdd[] = $group;
104-
}
105-
106-
return $groupsToAdd;
131+
return $groups->random($numGroupsToAdd)->all();
107132
}
108133

109134
private function ensureSampleImagesExist(): void
110135
{
111136
Storage::disk('public')->makeDirectory('sample-products');
112137

113-
$existingImages = Storage::disk('public')->files('sample-products');
114-
115-
if (count($existingImages) >= 15) {
116-
$this->command->info('Sample images already exist.');
117-
138+
if (count(Storage::disk('public')->files('sample-products')) >= 15) {
118139
return;
119140
}
120141

121-
$this->command->info('Downloading sample product images...');
122-
123142
for ($i = 1; $i <= 15; $i++) {
124-
$imagePath = "sample-products/{$i}.jpg";
125-
126-
if (Storage::disk('public')->exists($imagePath)) {
143+
if (Storage::disk('public')->exists("sample-products/{$i}.jpg")) {
127144
continue;
128145
}
129146

130-
try {
131-
$imageUrl = "https://picsum.photos/400/300?random={$i}";
132-
$response = Http::timeout(10)->get($imageUrl);
147+
$image = $this->generatePlaceholderImage($i);
148+
Storage::disk('public')->put("sample-products/{$i}.jpg", $image);
149+
}
150+
}
151+
152+
private function generatePlaceholderImage(int $index): string
153+
{
154+
$image = imagecreatetruecolor(400, 300);
155+
$hue = ($index * 24) % 360;
133156

134-
if ($response->successful()) {
135-
Storage::disk('public')->put($imagePath, $response->body());
136-
$this->command->info("Downloaded image {$i}/15");
137-
}
138-
} catch (Exception $e) {
139-
$this->command->warn("Failed to download image {$i}: ".$e->getMessage());
140-
}
157+
$this->drawBackground($image, $hue);
158+
$this->drawShapes($image, $hue);
159+
$this->drawOverlay($image, $hue);
160+
161+
ob_start();
162+
imagejpeg($image, null, 85);
163+
$imageData = ob_get_clean();
164+
imagedestroy($image);
165+
166+
return $imageData;
167+
}
168+
169+
private function drawBackground($image, int $hue): void
170+
{
171+
[$r, $g, $b] = $this->hslToRgb($hue / 360, 0.6, 0.5);
172+
$bgColor = imagecolorallocate($image, $r, $g, $b);
173+
imagefill($image, 0, 0, $bgColor);
174+
}
175+
176+
private function drawShapes($image, int $baseHue): void
177+
{
178+
for ($i = 0; $i < 50; $i++) {
179+
$hue = ($baseHue + rand(-30, 30)) / 360;
180+
[$r, $g, $b] = $this->hslToRgb($hue, rand(40, 80) / 100, rand(30, 70) / 100);
181+
$color = imagecolorallocate($image, $r, $g, $b);
182+
183+
match (rand(0, 2)) {
184+
0 => imagefilledellipse($image, rand(0, 400), rand(0, 300), rand(20, 100), rand(20, 100), $color),
185+
1 => imageline($image, rand(0, 400), rand(0, 300), rand(0, 400), rand(0, 300), $color),
186+
2 => imagefilledrectangle($image, rand(0, 400), rand(0, 300), rand(0, 400), rand(0, 300), $color),
187+
};
141188
}
189+
}
142190

143-
$this->command->info('Sample images ready!');
191+
private function drawOverlay($image, int $baseHue): void
192+
{
193+
for ($i = 0; $i < 3; $i++) {
194+
[$r, $g, $b] = $this->hslToRgb($baseHue / 360, 0.1, rand(80, 95) / 100);
195+
$overlayColor = imagecolorallocatealpha($image, $r, $g, $b, rand(90, 110));
196+
imagefilledellipse($image, rand(-50, 350), rand(-50, 250), rand(200, 400), rand(200, 400), $overlayColor);
197+
}
198+
}
199+
200+
private function hslToRgb(float $h, float $s, float $l): array
201+
{
202+
if ($s == 0) {
203+
$rgb = $l;
204+
205+
return [round($rgb * 255), round($rgb * 255), round($rgb * 255)];
206+
}
207+
208+
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
209+
$p = 2 * $l - $q;
210+
211+
return [
212+
round($this->hueToRgb($p, $q, $h + 1 / 3) * 255),
213+
round($this->hueToRgb($p, $q, $h) * 255),
214+
round($this->hueToRgb($p, $q, $h - 1 / 3) * 255),
215+
];
216+
}
217+
218+
private function hueToRgb(float $p, float $q, float $t): float
219+
{
220+
$t = match (true) {
221+
$t < 0 => $t + 1,
222+
$t > 1 => $t - 1,
223+
default => $t,
224+
};
225+
226+
return match (true) {
227+
$t < 1 / 6 => $p + ($q - $p) * 6 * $t,
228+
$t < 1 / 2 => $q,
229+
$t < 2 / 3 => $p + ($q - $p) * (2 / 3 - $t) * 6,
230+
default => $p,
231+
};
144232
}
145233

146234
private function ensureProductTypesExist(): void
@@ -170,31 +258,43 @@ private function ensureProductStatusesExist(): void
170258
}
171259
}
172260

173-
/**
174-
* Assign a random product status to a product data record, respecting tenancy.
175-
*/
176261
private function assignRandomProductStatus(ProductData $productData, ?int $tenantId): void
177262
{
178263
$tenantFK = config('eclipse-catalogue.tenancy.foreign_key');
179264

180-
// Get available product statuses for the tenant
181265
$query = ProductStatus::query();
182266

183267
if ($tenantFK && $tenantId !== null) {
184-
// Multi-tenant scenario: get statuses for this specific tenant
185268
$query->where($tenantFK, $tenantId);
186269
} elseif (! $tenantFK) {
187-
// Single-tenant scenario: get all statuses (site_id will be null)
188270
$query->whereNull('site_id');
189271
}
190272

191273
$availableStatuses = $query->get();
192274

193-
if ($availableStatuses->isNotEmpty()) {
194-
// Randomly select a status (with 70% chance of getting a status, 30% chance of no status)
195-
if (rand(1, 100) <= 70) {
196-
$randomStatus = $availableStatuses->random();
197-
$productData->update(['product_status_id' => $randomStatus->id]);
275+
if ($availableStatuses->isNotEmpty() && rand(1, 100) <= 70) {
276+
$randomStatus = $availableStatuses->random();
277+
$productData->update(['product_status_id' => $randomStatus->id]);
278+
}
279+
}
280+
281+
private function attachImagesToProducts($products): void
282+
{
283+
$productsWithImages = $products->random(10);
284+
285+
foreach ($productsWithImages as $index => $product) {
286+
$imageNumber = ($index % 15) + 1;
287+
$sourceFileName = "{$imageNumber}.jpg";
288+
$sourcePath = storage_path("app/public/sample-products/{$sourceFileName}");
289+
290+
if (file_exists($sourcePath)) {
291+
try {
292+
$product->addMedia($sourcePath)
293+
->preservingOriginal()
294+
->withCustomProperties(['is_cover' => true])
295+
->toMediaCollection('images', 'public');
296+
} catch (Exception $e) {
297+
}
198298
}
199299
}
200300
}

0 commit comments

Comments
 (0)