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 +136 -8 1.3.25 → 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 */
@@ -154,8 +214,13 @@
154 214 ->orWhere('product_variation_id', '[]');
155 215
156 216 }
157 217
218 + public function attrRelations(): \FluentCart\Framework\Database\Orm\Relations\HasMany
219 + {
220 + return $this->hasMany(AttributeRelation::class, 'object_id', 'id');
221 + }
222 +
158 223 public function order_items()
159 224 {
160 225 return $this->hasMany(OrderItem::class, 'object_id', 'id');
161 226 }
@@ -308,9 +373,15 @@
308 373 }
309 374 return false;
310 375 }
311 376
312 - 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
313 384 {
314 385 // Check if variation is active
315 386 if ($this->item_status !== 'active') {
316 387 return false;
@@ -322,9 +393,9 @@
322 393 // If stock management is disabled for this variation
323 394 if (!$this->manage_stock) {
324 395 // For bundle products, still check child items
325 396 if ($isBundleProduct) {
326 - return $this->isBundleChildrenInStock();
397 + return $this->isBundleChildrenInStock($bundleChildren);
327 398 }
328 399 // For regular products without stock management, check status
329 400 return $this->stock_status === Helper::IN_STOCK;
330 401 }
@@ -341,17 +412,53 @@
341 412 if (!$hasStock) {
342 413 return false;
343 414 }
344 415
345 - return $this->isBundleChildrenInStock();
416 + return $this->isBundleChildrenInStock($bundleChildren);
346 417 }
347 418
348 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 + /**
349 456 * Check if all bundle children are in stock
350 457 *
351 458 * @return bool
352 459 */
353 - protected function isBundleChildrenInStock(): bool
460 + protected function isBundleChildrenInStock($preloadedChildren = null): bool
354 461 {
355 462 $childIds = Arr::get($this->other_info, 'bundle_child_ids', []);
356 463
357 464 // No bundle children, consider as in stock
@@ -358,12 +465,33 @@
358 465 if (empty($childIds) || !is_array($childIds)) {
359 466 return true;
360 467 }
361 468
362 - // Get all bundle children variations
363 - $children = static::query()
364 - ->whereIn('id', $childIds)
365 - ->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 + }
366 494
367 495 // Check each child
368 496 foreach ($children as $child) {
369 497 // Child must be active