PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Helpers / ProductAdminHelper.php

ProductAdminHelper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Helpers/ProductAdminHelper.php

340 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Helpers;
4
5 use FluentCart\Api\Resource\ProductDownloadResource;
6 use FluentCart\Api\Resource\ProductVariationResource;
7 use FluentCart\App\Models\AttributeRelation;
8 use FluentCart\App\Models\AttributeTerm;
9 use FluentCart\App\Models\ProductMeta;
10 use FluentCart\App\Models\ProductVariation;
11 use FluentCart\App\Services\Helpers;
12 use FluentCart\App\Vite;
13 use FluentCart\Framework\Support\Arr;
14 use FluentCart\Framework\Support\Collection;
15
16 class ProductAdminHelper
17 {
18
19 /**
20 *
21 * @param $details
22 * @param $variants
23 */
24 public static function syncProduct($details, $variants)
25 {
26 $variationIds = [];
27 $variationType = Arr::get($details, 'variation_type', '');
28 $postId = Arr::get($details, 'post_id');
29 $variants = Arr::except($variants, ['*']);
30
31
32
33 foreach ($variants as $index => $variant) {
34 $variant['serial_index'] = $index + 1;
35 $variant['fulfillment_type'] = Arr::get(
36 $variant, 'fulfillment_type', Arr::get($details, 'fulfillment_type')
37 );
38 $variantId = Arr::get($variant, 'id');
39 if (empty($variantId)) {
40 $result = ProductVariationResource::create($variant);
41 } else {
42 $result = ProductVariationResource::update($variant, $variantId);
43 }
44
45 $variationIds[] = Arr::get($result, 'data.id');
46 $variants[$index]['id'] = Arr::get($result, 'data.id');
47 if ($variationType === \FluentCart\App\Helpers\Helper::PRODUCT_TYPE_SIMPLE) {
48 break;
49 }
50 }
51
52 self::deleteOrphanVariant($postId, $variationIds);
53
54 ProductDownloadResource::delete(null, ['type' => 'byProduct', 'post_id' => $postId]);
55 return ProductVariation::query()->where('post_id', $postId)->get();
56 }
57
58 /**
59 * Syncing advance variations
60 *
61 * @param $srcDetails
62 * @param $variations
63 * @param array $variantProductDetails
64 * @return mixed
65 */
66 public static function syncAdvanceVariations($srcDetails, $variations, array $variantProductDetails = [])
67 {
68 $formattedVariations = [];
69
70 foreach ($variations as $variation) {
71 if (!empty($variation['variants'])) {
72 $formattedVariations[] = $variation['variants'];
73 }
74 }
75
76 $variants = self::generateVariationSets($formattedVariations);
77
78 // Guard: no terms selected → nothing to generate. Calling deleteOrphanVariant
79 // with an empty keep-list would match every row and wipe all variants for
80 // the product. Return empty rather than destroying existing data.
81 if (empty($variants)) {
82 return new Collection();
83 }
84
85 $srcDetails->load('product');
86
87 // Preload every AttributeTerm referenced in this sync — one SELECT instead
88 // of one per term per variant (avoids N+1 on the term lookup).
89 $allTermIds = array_unique(array_merge(...array_map('array_values', $variants)));
90 $termsList = AttributeTerm::query()->whereIn('id', $allTermIds)->get();
91 $termsMap = [];
92 foreach ($termsList as $term) {
93 $termsMap[$term->id] = $term;
94 }
95
96 $variantIds = [];
97 $newRelations = [];
98
99 $db = ProductVariation::query()->getConnection();
100 try {
101 $db->beginTransaction();
102
103 foreach ($variants as $index => $variant) {
104 asort($variant, SORT_NUMERIC);
105
106 $variationIdentifier = implode('_', $variant);
107
108 $variantData = [
109 'post_id' => $srcDetails->post_id,
110 'serial_index' => $index + 1,
111 'stock' => 100,
112 'item_price' => 0,
113 'fulfillment_type' => 'physical',
114 'variation_title' => $srcDetails->product->post_title,
115 'variation_identifier' => $variationIdentifier,
116 'other_info' => [
117 'variant' => array_values($variant),
118 ],
119 ];
120
121 $exist = ProductVariation::query()
122 ->where('post_id', $srcDetails->post_id)
123 ->where('variation_identifier', $variationIdentifier)
124 ->first();
125
126 if ($exist) {
127 $exist->serial_index = $index + 1;
128 $exist->save();
129 } else {
130 $exist = ProductVariation::create($variantData);
131 }
132
133 $variantIds[] = $exist->id;
134
135 foreach ($variant as $termId) {
136 if (!isset($termsMap[$termId])) {
137 continue;
138 }
139 $term = $termsMap[$termId];
140 $newRelations[] = [
141 'term_id' => $term->id,
142 'object_id' => $exist->id,
143 'group_id' => $term->group_id,
144 ];
145 }
146 }
147
148 // Bulk-insert relations — fetch existing keys first (one query) then
149 // insert only the missing ones in 100-row chunks instead of one
150 // firstOrCreate per term per variant.
151 if (!empty($newRelations)) {
152 $existingKeys = [];
153 $existing = AttributeRelation::query()->whereIn('object_id', $variantIds)->get();
154 foreach ($existing as $rel) {
155 $existingKeys[$rel->object_id . ':' . $rel->term_id] = true;
156 }
157 $toInsert = array_values(array_filter($newRelations, function ($r) use ($existingKeys) {
158 return !isset($existingKeys[$r['object_id'] . ':' . $r['term_id']]);
159 }));
160 foreach (array_chunk($toInsert, 100) as $chunk) {
161 AttributeRelation::query()->insert($chunk);
162 }
163 }
164
165 self::deleteOrphanVariant($srcDetails->post_id, $variantIds);
166
167 $db->commit();
168 } catch (\Exception $e) {
169 $db->rollBack();
170 throw $e;
171 }
172
173 return ProductVariation::query()->whereIn('id', $variantIds)->get();
174 }
175
176
177 /**
178 * Build a bounded, human-readable summary of variation titles for activity
179 * logs. When the count is within $limit the full list is returned; only when
180 * there are MORE than $limit titles do we sample the first $limit and append
181 * "and N more", so large combination sets don't bloat the log content.
182 *
183 * @param array $titles
184 * @param int $limit
185 * @return string
186 */
187 public static function summarizeVariationTitles(array $titles, $limit = 5)
188 {
189 $titles = array_values(array_filter($titles, function ($title) {
190 return $title !== null && $title !== '';
191 }));
192
193 $total = count($titles);
194
195 if ($total <= $limit) {
196 return implode(', ', $titles);
197 }
198
199 return sprintf(
200 /* translators: %1$s: sample of variation titles, %2$d: number of remaining variations not listed */
201 __('%1$s and %2$d more', 'fluent-cart'),
202 implode(', ', array_slice($titles, 0, $limit)),
203 $total - $limit
204 );
205 }
206
207 /**
208 *
209 * @param $productId
210 * @param array $childrenIdsWeWantToKeepSafe
211 * @param string $reason Human-readable cause for the deletion, shown in the
212 * log content (e.g. "the variation type was changed to
213 * 'Simple'"). Defaults to a generic, always-true phrase
214 * so the message never claims the wrong cause — this is
215 * called from several flows (Simple switch, option/group
216 * changes, regeneration), not only the Simple switch.
217 * @return mixed
218 */
219 public static function deleteOrphanVariant($productId, array $childrenIdsWeWantToKeepSafe = [], $reason = '')
220 {
221 $orphans = ProductVariation::query()
222 ->select(['id', 'variation_title'])
223 ->where('post_id', $productId)
224 ->whereNotIn('id', $childrenIdsWeWantToKeepSafe)
225 ->get();
226
227 if ($orphans->isEmpty()) {
228 return 0;
229 }
230
231 $orphanIds = $orphans->pluck('id')->toArray();
232
233 // Summarize rather than join every title — a product can have hundreds
234 // of combinations and the full list bloats the activity-log content.
235 // Below the cap the full list is kept; above it we sample + "and N more".
236 $variationTitles = static::summarizeVariationTitles(
237 $orphans->pluck('variation_title')->all()
238 );
239
240 if ($reason === '') {
241 $reason = __('the product variations were updated', 'fluent-cart');
242 }
243
244 fluent_cart_success_log(
245 sprintf(
246 /* translators: %1$s: number of variations deleted */
247 __('%1$s Pricing deleted', 'fluent-cart'),
248 $orphans->count()
249 ),
250 sprintf(
251 /* translators: %1$s: variation titles, %2$s: reason the pricings were deleted */
252 _n(
253 '%1$s Pricing is deleted, while %2$s',
254 "%1\$s Pricing's are deleted, while %2\$s",
255 $orphans->count(),
256 'fluent-cart'
257 ),
258 $variationTitles,
259 $reason
260 ),
261 [
262 'module_name' => 'Product',
263 'module_id' => 0,
264 'module_type' => ProductVariation::class,
265 ]
266 );
267
268 // Bulk query-builder deletes bypass the ProductVariation::boot() deleting
269 // event, so neither $model->attrMap()->delete() nor deleteVariationMedia()
270 // fires. Explicitly purge both tables for every orphaned variation before
271 // deleting the variations themselves to prevent orphaned rows.
272 AttributeRelation::query()->whereIn('object_id', $orphanIds)->delete();
273 ProductMeta::query()->where('object_type', 'product_variant_info')->whereIn('object_id', $orphanIds)->delete();
274
275 $orphanIds = ProductVariation::query()
276 ->select('id')
277 ->where('post_id', $productId)
278 ->whereNotIn('id', $childrenIdsWeWantToKeepSafe)
279 ->pluck('id')
280 ->toArray();
281
282 // Attribute relations table only exists when pro is active — guard before querying.
283 if ($orphanIds && \FluentCart\App\App::isProActive()) {
284 AttributeRelation::query()->whereIn('object_id', $orphanIds)->delete();
285 }
286
287 return ProductVariation::query()
288 ->where('post_id', $productId)
289 ->whereNotIn('id', $childrenIdsWeWantToKeepSafe)
290 ->delete();
291 }
292
293 public static function generateVariationSets($formattedVariations, $i = 0)
294 {
295 if (!isset($formattedVariations[$i])) {
296 return [];
297 }
298
299 $result = [];
300
301 /**
302 * Fix: With only one variation group it does not give proper result.
303 *
304 */
305 if ($i === 0 && count($formattedVariations) === 1 && is_array($formattedVariations[0])) {
306
307 foreach ($formattedVariations[0] as $item) {
308 $result[] = [$item];
309 }
310
311 return $result;
312 }
313
314
315 if ($i == count($formattedVariations) - 1) {
316 return $formattedVariations[$i];
317 }
318
319 // get combinations from subsequent arrays
320 $tmp = self::generateVariationSets($formattedVariations, $i + 1);
321
322
323 // concat each array from tmp with each element from $arrays[$i]
324 foreach ($formattedVariations[$i] as $v) {
325 foreach ($tmp as $t) {
326 $result[] = is_array($t) ?
327 array_merge([$v], $t) :
328 [$v, $t];
329 }
330 }
331
332 return $result;
333 }
334
335 public static function getFeaturedMedia($featuredMedia): string
336 {
337 return !empty($featuredMedia) ? Arr::get($featuredMedia, 'url') : Vite::getAssetUrl('images/placeholder.svg');
338 }
339 }
340