PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 1.6.6 1.6.5 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 All 50 releases
fluent-cart / app / Helpers / ProductAdminHelper.php

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

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