PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.1
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
fluent-cart / app / Helpers / AttributeHelper.php

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

283 lines 10.9 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\App\Models\AttributeGroup;
6 use FluentCart\App\Models\AttributeRelation;
7 use FluentCart\Framework\Support\Arr;
8 use FluentCart\Framework\Support\Str;
9
10 /**
11 * Helper for the store attribute library and per-item attribute snapshots.
12 *
13 * @package FluentCart\App\Helpers
14 *
15 * @version 1.0.0
16 */
17 class AttributeHelper
18 {
19 /**
20 * Return the store attribute library, or a single group set by slug.
21 *
22 * Loads every attribute group (with its terms) ONCE per request into a
23 * static cache, then serves all later calls from memory — so resolving
24 * variant attributes for many cart/order items never re-queries the DB.
25 *
26 * Shape — groups keyed by slug; each group carries its meta plus every term
27 * keyed by slug DIRECTLY on the group, so `color.red` resolves the term:
28 * [
29 * 'color' => [
30 * 'title' => 'Color',
31 * 'slug' => 'color',
32 * 'type' => 'color',
33 * 'red' => ['title' => 'Red', 'slug' => 'red', 'settings' => [...]],
34 * 'blue' => ['title' => 'Blue', 'slug' => 'blue', 'settings' => [...]],
35 * ],
36 * ]
37 *
38 * Note: term slugs share the group array with the reserved meta keys
39 * `title`/`slug`/`type`; product attribute terms never use those slugs.
40 *
41 * @param string $attrSlug Group slug (e.g. 'color', 'size'). Empty = all.
42 * @return array Single group set, the whole library keyed by group slug when
43 * $attrSlug is empty, or [] when the slug is not found.
44 */
45 public static function getStoreProductAttributeSet($attrSlug = '')
46 {
47 static $allAttributes = null;
48
49 if ($allAttributes === null) {
50 // Groups keyed by slug; each group merges its meta with every term
51 // keyed by slug (flat), so `color.red` resolves the term directly.
52 $allAttributes = AttributeGroup::query()
53 ->with(['terms' => function ($query) {
54 $query->orderBy('serial', 'ASC');
55 }])
56 ->orderBy('serial', 'ASC')
57 ->get()
58 ->keyBy('slug')
59 ->map(function ($group) {
60 $groupSettings = is_array($group->settings) ? $group->settings : [];
61
62 $terms = $group->terms->keyBy('slug')->map(function ($term) {
63 return [
64 'title' => $term->title,
65 'slug' => $term->slug,
66 'settings' => is_array($term->settings) ? $term->settings : [],
67 ];
68 })->toArray();
69
70 return array_merge([
71 'title' => $group->title,
72 'slug' => $group->slug,
73 'type' => Arr::get($groupSettings, 'type', 'options'),
74 ], $terms);
75 })
76 ->toArray();
77 }
78
79 if ($attrSlug) {
80 return Arr::get($allAttributes, $attrSlug, []);
81 }
82
83 return $allAttributes;
84 }
85
86 /**
87 * Build the `item_attributes` snapshot for a single cart/order line item.
88 *
89 * Stored at `other_info['item_attributes']` on cart_data items and order_items.
90 * FluentCart's own attributes are keyed `pa_{group_slug}`; the value is the
91 * term TITLE and slug is the term SLUG — frozen at the moment it is written so
92 * later renames in the attribute library never rewrite past orders. Third-party
93 * providers append their own entries (WITHOUT the `pa_` prefix) via the
94 * `fluent_cart/item_attributes` filter — they supply their own group slug as
95 * the key plus value/slug.
96 *
97 * Output shape:
98 * [
99 * 'pa_color' => ['value' => 'Red', 'slug' => 'red'],
100 * 'pa_size' => ['value' => 'XS', 'slug' => 'xs'],
101 * 'fluent_booking_start' => ['value' => '2026-06-29 12:12:00', 'slug' => 'fluent_booking'],
102 * ]
103 *
104 * @param int $variationId Product variation id (order/cart item object_id).
105 * @param int $productId Owning product id (passed to the filter for context).
106 * @return array
107 */
108 public static function getProductItemAttributes($variationId, $productId = 0)
109 {
110 $atts = [];
111
112 $variationId = (int) $variationId;
113
114 if ($variationId) {
115 $relations = AttributeRelation::query()
116 ->where('object_id', $variationId)
117 ->with(['group', 'term'])
118 ->get();
119
120 foreach ($relations as $relation) {
121 $group = $relation->group;
122 $term = $relation->term;
123
124 if (!$group || !$term) {
125 continue;
126 }
127
128 // Our own attributes carry the `pa_` prefix on the group slug.
129 $atts['pa_' . $group->slug] = [
130 'value' => $term->title,
131 'slug' => $term->slug,
132 ];
133 }
134 }
135
136 // Third-party attributes are appended without the `pa_` prefix — providers
137 // key by their own group slug and supply value/slug themselves.
138 return apply_filters('fluent_cart/item_attributes', $atts, [
139 'variation_id' => $variationId,
140 'product_id' => (int) $productId,
141 ]);
142 }
143
144 /**
145 * The store's attribute groups as a lightweight slug => label/type map.
146 *
147 * Sourced from the request-cached `getStoreProductAttributeSet()` registry,
148 * so this is only the current (live) group labels — used to resolve the
149 * display title of a frozen `pa_{slug}` snapshot entry.
150 *
151 * @return array e.g. ['color' => ['title' => 'Color', 'slug' => 'color', 'type' => 'color']]
152 */
153 public static function getMainAttributes()
154 {
155 $mainAtts = [];
156
157 foreach (self::getStoreProductAttributeSet() as $slug => $group) {
158 $mainAtts[$slug] = [
159 'title' => Arr::get($group, 'title', $slug),
160 'slug' => Arr::get($group, 'slug', $slug),
161 'type' => Arr::get($group, 'type', 'options'),
162 ];
163 }
164
165 return $mainAtts;
166 }
167
168 /**
169 * Resolve a line item's stored `item_attributes` snapshot into display rows.
170 *
171 * FluentCart attributes (keyed `pa_{group_slug}`) are resolved against the
172 * live group library for their display title; the term value/slug come from
173 * the frozen snapshot. A `pa_*` entry whose group no longer exists is
174 * skipped. Third-party (un-prefixed) entries are resolved through the
175 * `fluent_cart/item_display_attr_{key}` filter so the owning plugin can
176 * shape its own label/value.
177 *
178 * @param array $itemAttributes other_info['item_attributes'] snapshot.
179 * @param mixed $item Owning cart/order item (passed to filters).
180 * @param string $scope 'cart' | 'order_item' (passed to filters).
181 * @return array Keyed by attr key: ['display_title','attr_key','slug','display_value','is_system']
182 */
183 public static function getDisplayAttributes(array $itemAttributes, $item = null, $scope = 'cart')
184 {
185 if(!$itemAttributes) {
186 return [];
187 }
188
189 $mainAtts = self::getMainAttributes();
190 $displayAtts = [];
191
192 foreach ($itemAttributes as $key => $value) {
193 if (strpos($key, 'pa_') === 0) {
194 $groupSlug = substr($key, 3);
195
196 // Group was removed from the library — drop the stale entry.
197 if (!isset($mainAtts[$groupSlug])) {
198 $displayAtts[$key] = [
199 'display_title' => Str::of($groupSlug)->title(),
200 'attr_key' => $key,
201 'slug' => $key,
202 'display_value' => Arr::get($value, 'value', ''),
203 'is_system' => true,
204 ];
205 continue;
206 }
207
208 $mainAtt = $mainAtts[$groupSlug];
209
210 $displayAtts[$key] = [
211 'display_title' => $mainAtt['title'],
212 'attr_key' => $key,
213 'slug' => $mainAtt['slug'],
214 'display_value' => Arr::get($value, 'value', ''),
215 'is_system' => true,
216 ];
217
218 continue;
219 }
220
221 // Third-party attribute — let the owning plugin shape the display row.
222 $displayAtts[$key] = apply_filters('fluent_cart/item_display_attr_' . $key, [
223 'display_title' => $key,
224 'attr_key' => $key,
225 'slug' => Arr::get($value, 'slug', $key),
226 'display_value' => Arr::get($value, 'value', ''),
227 'is_system' => false,
228 ], [
229 'attr' => $value,
230 'item' => $item,
231 'scope' => $scope,
232 ]);
233 }
234
235 $displayAtts = apply_filters('fluent_cart/item_display_attr', $displayAtts, [
236 'item' => $item,
237 'scope' => $scope,
238 ]);
239
240 // Drop rows that resolved to an empty value (e.g. a provider opted out).
241 return array_filter($displayAtts, function ($attr) {
242 return !empty($attr['display_value']);
243 });
244 }
245
246 /**
247 * Render a line item's attributes as a single inline string.
248 *
249 * @param array $itemAttributes other_info['item_attributes'] snapshot.
250 * @param mixed $item Owning cart/order item (passed to filters).
251 * @param string $scope 'cart' | 'order_item'.
252 * @param string $separator Glue between pairs (default ' | ').
253 * @return string e.g. "Color: Red | Size: XS"
254 */
255 public static function getDisplayAttributesString(array $itemAttributes, $item = null, $scope = 'cart', $separator = ' | ')
256 {
257 $displayAtts = self::getDisplayAttributes($itemAttributes, $item, $scope);
258 if(!$displayAtts || !is_array($displayAtts)) {
259 return '';
260 }
261
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;
269 }
270
271 $string = implode($separator, $parts);
272
273 // Let integrators render the combination in their own format — they get
274 // the default string plus the resolved rows to rebuild from scratch.
275 return apply_filters('fluent_cart/item_display_attr_string', $string, [
276 'display_atts' => $displayAtts,
277 'item' => $item,
278 'scope' => $scope,
279 'separator' => $separator,
280 ]);
281 }
282 }
283