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
← All changes | app/Models/ProductVariation.php +131 -8 1.5.5 → 1.7.0 View file →
@@ -3,8 +3,9 @@
3 3 namespace FluentCart\App\Models;
4 4
5 5 use FluentCart\Api\ModuleSettings;
6 6 use FluentCart\App\App;
7 +use FluentCart\App\Helpers\AttributeHelper;
7 8 use FluentCart\App\Helpers\Helper;
8 9 use FluentCart\App\Helpers\Status;
9 10 use FluentCart\App\Models\Concerns\CanSearch;
10 11 use FluentCart\App\Models\Concerns\CanUpdateBatch;
@@ -130,8 +131,67 @@
130 131 return $this->belongsTo(ShippingClass::class, 'shipping_class', 'id');
131 132 }
132 133
133 134 /**
135 + * Labeled attribute combination for this variation, e.g. "Color: Red | Size: XS".
136 + *
137 + * Uses the batched `variation_display_title` when
138 + * AttributeHelper::attachVariationDisplayTitles() has already resolved it,
139 + * and otherwise resolves on demand — so callers get a correct value either
140 + * way, and a correctly batched caller costs no extra queries.
141 + *
142 + * @return string Falls back to the stored variation title.
143 + */
144 + public function getVariationLabel(): string
145 + {
146 + $resolved = (string) ($this->variation_display_title ?? '');
147 +
148 + if ($resolved !== '') {
149 + return $resolved;
150 + }
151 +
152 + $variationType = $this->product_detail ? $this->product_detail->variation_type : '';
153 +
154 + $label = AttributeHelper::getDisplayAttributesString(
155 + AttributeHelper::getProductItemAttributes($this->id, $this->post_id),
156 + [
157 + 'title' => $this->variation_title,
158 + 'variation_type' => $variationType,
159 + 'other_info' => ['variation_type' => $variationType],
160 + ],
161 + 'order_item'
162 + );
163 +
164 + return $label !== '' ? $label : (string) $this->variation_title;
165 + }
166 +
167 + /**
168 + * Full display title for this variation: "<product> - <attribute combination>".
169 + *
170 + * The catalogue-side counterpart to OrderItem::getDisplayTitle(), which
171 + * composes the same shape from a frozen line item. Product feeds, plan
172 + * names and CLI output all need the product name alongside the variation,
173 + * and each was concatenating it themselves.
174 + *
175 + * @return string
176 + */
177 + public function getDisplayTitle(): string
178 + {
179 + $label = $this->getVariationLabel();
180 + $productTitle = $this->product ? (string) $this->product->post_title : '';
181 +
182 + if ($productTitle === '' || $productTitle === $label) {
183 + return $label;
184 + }
185 +
186 + if ($label === '') {
187 + return $productTitle;
188 + }
189 +
190 + return $productTitle . ' - ' . $label;
191 + }
192 +
193 + /**
134 194 * One2One: Product Variation belongs to one Product detail
135 195 *
136 196 * @return \FluentCart\Framework\Database\Orm\Relations\BelongsTo
137 197 */
@@ -313,9 +373,15 @@
313 373 }
314 374 return false;
315 375 }
316 376
317 - public function isStock(): bool
377 + /**
378 + * @param array|null $bundleChildren Bundle child rows keyed by id, from
379 + * loadBundleChildren(), when the caller has several bundle variations
380 + * to ask about and wants one query for all their children instead of
381 + * one per variation. Null queries this variation's own children.
382 + */
383 + public function isStock($bundleChildren = null): bool
318 384 {
319 385 // Check if variation is active
320 386 if ($this->item_status !== 'active') {
321 387 return false;
@@ -327,9 +393,9 @@
327 393 // If stock management is disabled for this variation
328 394 if (!$this->manage_stock) {
329 395 // For bundle products, still check child items
330 396 if ($isBundleProduct) {
331 - return $this->isBundleChildrenInStock();
397 + return $this->isBundleChildrenInStock($bundleChildren);
332 398 }
333 399 // For regular products without stock management, check status
334 400 return $this->stock_status === Helper::IN_STOCK;
335 401 }
@@ -346,17 +412,53 @@
346 412 if (!$hasStock) {
347 413 return false;
348 414 }
349 415
350 - return $this->isBundleChildrenInStock();
416 + return $this->isBundleChildrenInStock($bundleChildren);
351 417 }
352 418
353 419 /**
420 + * The bundle children of several variations in one query, keyed by id,
421 + * for isStock($bundleChildren). Only the columns the stock check reads.
422 + *
423 + * @param iterable $variations
424 + * @return array<int, static>
425 + */
426 + public static function loadBundleChildren($variations): array
427 + {
428 + $childIds = [];
429 + foreach ($variations as $variation) {
430 + $ids = Arr::get($variation->other_info, 'bundle_child_ids', []);
431 + if (is_array($ids)) {
432 + foreach ($ids as $id) {
433 + $childIds[(int) $id] = true;
434 + }
435 + }
436 + }
437 + unset($childIds[0]);
438 +
439 + if (!$childIds) {
440 + return [];
441 + }
442 +
443 + $children = static::query()
444 + ->whereIn('id', array_keys($childIds))
445 + ->get(['id', 'manage_stock', 'available', 'stock_status', 'item_status', 'post_id', 'other_info']);
446 +
447 + $byId = [];
448 + foreach ($children as $child) {
449 + $byId[(int) $child->id] = $child;
450 + }
451 +
452 + return $byId;
453 + }
454 +
455 + /**
354 456 * Check if all bundle children are in stock
355 457 *
356 458 * @return bool
357 459 */
358 - protected function isBundleChildrenInStock(): bool
460 + protected function isBundleChildrenInStock($preloadedChildren = null): bool
359 461 {
360 462 $childIds = Arr::get($this->other_info, 'bundle_child_ids', []);
361 463
362 464 // No bundle children, consider as in stock
@@ -363,12 +465,33 @@
363 465 if (empty($childIds) || !is_array($childIds)) {
364 466 return true;
365 467 }
366 468
367 - // Get all bundle children variations
368 - $children = static::query()
369 - ->whereIn('id', $childIds)
370 - ->get(['id', 'manage_stock', 'available', 'stock_status', 'item_status', 'post_id', 'other_info']);
469 + $childIds = array_values(array_unique(array_map('intval', $childIds)));
470 +
471 + if (is_array($preloadedChildren)) {
472 + // From loadBundleChildren().
473 + $children = [];
474 + foreach ($childIds as $childId) {
475 + if (isset($preloadedChildren[$childId])) {
476 + $children[] = $preloadedChildren[$childId];
477 + }
478 + }
479 + } else {
480 + // Get all bundle children variations
481 + $children = static::query()
482 + ->whereIn('id', $childIds)
483 + ->get(['id', 'manage_stock', 'available', 'stock_status', 'item_status', 'post_id', 'other_info'])
484 + ->all();
485 + }
486 +
487 + // A child the bundle names but that no longer exists cannot be
488 + // supplied, so the bundle is not in stock. Same rule on both paths:
489 + // a row missing from the map and a row missing from the query are
490 + // the same deleted variation.
491 + if (count($children) !== count($childIds)) {
492 + return false;
493 + }
371 494
372 495 // Check each child
373 496 foreach ($children as $child) {
374 497 // Child must be active