PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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 1.3.19 trunk All 48 releases
← All changes | app/Helpers/AttributeHelper.php +176 -14 1.5.1 → 1.6.5 View file →
@@ -141,8 +141,119 @@
141 141 ]);
142 142 }
143 143
144 144 /**
145 + * Batched variant of getProductItemAttributes — resolves the item_attributes
146 + * snapshot for many variations with a SINGLE whereIn query, so cart writes
147 + * carrying several unsnapshotted items don't run one query per item. The
148 + * per-item `fluent_cart/item_attributes` filter still fires for each variation.
149 + *
150 + * @param array $variationIds Variation (object) ids.
151 + * @param array $productIds Optional variationId => productId map for filter context.
152 + * @return array variationId => item_attributes
153 + */
154 + public static function getProductItemsAttributes($variationIds, $productIds = [])
155 + {
156 + $variationIds = array_values(array_unique(array_filter(array_map('intval', (array) $variationIds))));
157 +
158 + if (!$variationIds) {
159 + return [];
160 + }
161 +
162 + $productIdMap = (array) $productIds;
163 +
164 + $relationsByVariation = AttributeRelation::query()
165 + ->whereIn('object_id', $variationIds)
166 + ->with(['group', 'term'])
167 + ->get()
168 + ->groupBy('object_id');
169 +
170 + $attributesByVariation = [];
171 + foreach ($variationIds as $variationId) {
172 + $atts = [];
173 +
174 + foreach ($relationsByVariation->get($variationId, []) as $relation) {
175 + $group = $relation->group;
176 + $term = $relation->term;
177 +
178 + if (!$group || !$term) {
179 + continue;
180 + }
181 +
182 + // Our own attributes carry the `pa_` prefix on the group slug.
183 + $atts['pa_' . $group->slug] = [
184 + 'value' => $term->title,
185 + 'slug' => $term->slug,
186 + ];
187 + }
188 +
189 + // Third-party attributes are appended without the `pa_` prefix —
190 + // providers key by their own group slug and supply value/slug.
191 + $attributesByVariation[$variationId] = apply_filters('fluent_cart/item_attributes', $atts, [
192 + 'variation_id' => $variationId,
193 + 'product_id' => (int) Arr::get($productIdMap, $variationId, 0),
194 + ]);
195 + }
196 +
197 + return $attributesByVariation;
198 + }
199 +
200 + /**
201 + * Attach a resolved `variation_display_title` ("Color: Red | Size: S") to every
202 + * variation of the given products — the picker/list-side mirror of the order
203 + * item display.
204 + *
205 + * Resolves the attribute snapshot for ALL variations in ONE batched query (no
206 + * per-variant N+1), and only touches products whose `detail.variants` relation
207 + * is eager-loaded — so callers that don't load variants pay nothing. Variations
208 + * are mutated in place; the value falls back to the raw variation_title for
209 + * simple products. Reusable across any product-list endpoint.
210 + *
211 + * @param iterable $products Product models with `detail.variants` loaded.
212 + * @return void
213 + */
214 + public static function attachVariationDisplayTitles($products)
215 + {
216 + // Single pass over the product tree: collect variation ids for the batched
217 + // lookup and keep a flat list of variants (with their type) to fill afterwards.
218 + $productIdByVariation = [];
219 + $pendingVariants = [];
220 + foreach ($products as $product) {
221 + $variants = ($product->detail && $product->detail->relationLoaded('variants')) ? $product->detail->variants : null;
222 + if (!$variants) {
223 + continue;
224 + }
225 + $variationType = $product->detail->variation_type;
226 + foreach ($variants as $variant) {
227 + $productIdByVariation[$variant->id] = (int) $product->ID;
228 + $pendingVariants[] = ['variant' => $variant, 'variation_type' => $variationType];
229 + }
230 + }
231 +
232 + if (!$pendingVariants) {
233 + return;
234 + }
235 +
236 + // ONE batched query resolves the attribute snapshot for every variation.
237 + $attributesByVariation = self::getProductItemsAttributes(array_keys($productIdByVariation), $productIdByVariation);
238 +
239 + foreach ($pendingVariants as $pendingVariant) {
240 + $variant = $pendingVariant['variant'];
241 + $variationType = $pendingVariant['variation_type'];
242 + $displayTitle = self::getDisplayAttributesString(
243 + Arr::get($attributesByVariation, $variant->id, []),
244 + [
245 + 'title' => $variant->variation_title,
246 + 'variation_type' => $variationType,
247 + 'other_info' => ['variation_type' => $variationType],
248 + ],
249 + 'order_item'
250 + );
251 + $variant->variation_display_title = $displayTitle !== '' ? $displayTitle : $variant->variation_title;
252 + }
253 + }
254 +
255 + /**
145 256 * The store's attribute groups as a lightweight slug => label/type map.
146 257 *
147 258 * Sourced from the request-cached `getStoreProductAttributeSet()` registry,
148 259 * so this is only the current (live) group labels — used to resolve the
@@ -243,40 +354,91 @@
243 354 });
244 355 }
245 356
246 357 /**
247 - * Render a line item's attributes as a single inline string.
358 + * Render a line item's variation display string.
248 359 *
360 + * Returns the labeled attribute combination ("Color: Red | Size: XS") when
361 + * the item carries an attribute snapshot. When no attributes resolve it
362 + * falls back to the item's variation title — staying empty for simple
363 + * products whose title equals their post_title (no real variation). So
364 + * callers can use the return value directly without their own fallback.
365 + *
249 366 * @param array $itemAttributes other_info['item_attributes'] snapshot.
250 - * @param mixed $item Owning cart/order item (passed to filters).
367 + * @param mixed $item Owning cart/order item (model or array).
251 368 * @param string $scope 'cart' | 'order_item'.
252 369 * @param string $separator Glue between pairs (default ' | ').
253 - * @return string e.g. "Color: Red | Size: XS"
370 + * @return string e.g. "Color: Red | Size: XS", the variation title, or ''
254 371 */
255 372 public static function getDisplayAttributesString(array $itemAttributes, $item = null, $scope = 'cart', $separator = ' | ')
256 373 {
257 374 $displayAtts = self::getDisplayAttributes($itemAttributes, $item, $scope);
258 - if(!$displayAtts || !is_array($displayAtts)) {
259 - return '';
375 +
376 + $parts = [];
377 + if (is_array($displayAtts)) {
378 + foreach ($displayAtts as $attr) {
379 + $title = Arr::get($attr, 'display_title', '');
380 + $value = Arr::get($attr, 'display_value', '');
381 + // Skip the "Label: " prefix when the title is missing, otherwise
382 + // we would render a stray leading colon (": Red").
383 + $parts[] = $title !== '' ? $title . ': ' . $value : $value;
384 + }
260 385 }
261 386
262 - $parts = [];
263 - foreach ($displayAtts as $attr) {
264 - $title = Arr::get($attr, 'display_title', '');
265 - $value = Arr::get($attr, 'display_value', '');
266 - // Skip the "Label: " prefix when the title is missing, otherwise we
267 - // would render a stray leading colon (": Red").
268 - $parts[] = $title !== '' ? $title . ': ' . $value : $value;
387 + $displayTitleString = implode($separator, $parts);
388 +
389 + // variation_type lives in other_info on order items, but cart items carry
390 + // it at the root level too — accept either so the advanced-variation check
391 + // works the same in cart, checkout and order contexts.
392 + $variationType = Arr::get($item, 'other_info.variation_type', '');
393 + if ($variationType === '') {
394 + $variationType = is_object($item)
395 + ? (string) ($item->variation_type ?? '')
396 + : (string) Arr::get($item, 'variation_type', '');
269 397 }
270 398
271 - $string = implode($separator, $parts);
399 + // Non-advanced items prefix the variation title ("<title> | <attributes>")
400 + // since their attributes (third-party injected) don't name the product;
401 + // advanced variations skip it as their combination is self-describing.
402 + if ($displayTitleString !== '' && $itemAttributes &&
403 + $variationType !== Helper::PRODUCT_TYPE_ADVANCE_VARIATION
404 + ) {
405 + $title = is_object($item) ? (string) ($item->title ?? '') : (string) Arr::get($item, 'title', '');
272 406
407 + if ($title !== '' && strpos($displayTitleString, $title) !== 0) {
408 + $displayTitleString = $title . ' | ' . $displayTitleString;
409 + }
410 + }
411 +
412 + if ($displayTitleString === '') {
413 + $displayTitleString = self::variationTitleFallback($item);
414 + }
415 +
273 416 // Let integrators render the combination in their own format — they get
274 417 // the default string plus the resolved rows to rebuild from scratch.
275 - return apply_filters('fluent_cart/item_display_attr_string', $string, [
418 + return apply_filters('fluent_cart/item_display_attr_string', $displayTitleString, [
276 419 'display_atts' => $displayAtts,
277 420 'item' => $item,
278 421 'scope' => $scope,
279 422 'separator' => $separator,
280 423 ]);
424 + }
425 +
426 + /**
427 + * Variation title used when no attributes resolve — empty for simple
428 + * products (title === post_title), otherwise the variation title.
429 + *
430 + * @param mixed $item Cart/order item model or array.
431 + * @return string
432 + */
433 + protected static function variationTitleFallback($item)
434 + {
435 + if ($item === null) {
436 + return '';
437 + }
438 +
439 + $postTitle = is_object($item) ? (string) ($item->post_title ?? '') : (string) Arr::get($item, 'post_title', '');
440 + $title = is_object($item) ? (string) ($item->title ?? '') : (string) Arr::get($item, 'title', '');
441 +
442 + return $postTitle === $title ? '' : $title;
281 443 }
282 444 }