PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 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 All 51 releases
← All changes | api/Resource/FrontendResource/CartResource.php +98 -13 1.4.0 → 1.7.1 View file →
@@ -19,8 +19,29 @@
19 19
20 20 class CartResource extends BaseResourceApi
21 21 {
22 22
23 + /**
24 + * Per-request memo for get(). In production every HTTP request runs in a
25 + * fresh PHP process, so this lives exactly one request. Long-running
26 + * processes that simulate multiple requests (test suites, CLI) must clear
27 + * it between simulated requests via resetCartCache() — as a
28 + * function-static it was unreachable and leaked the first request's cart
29 + * into every subsequent one.
30 + *
31 + * Only a resolved Cart is memoized; a null ("no cart") result is
32 + * deliberately re-queried on the next call — matching the original
33 + * function-static behavior, where isset(null) === false.
34 + *
35 + * @var Cart|null|false false = not resolved yet
36 + */
37 + private static $cartCache = false;
38 +
39 + public static function resetCartCache(): void
40 + {
41 + static::$cartCache = false;
42 + }
43 +
23 44 public static function getQuery(): Builder
24 45 {
25 46 return Cart::query();
26 47 }
@@ -35,11 +56,15 @@
35 56 $variation = Arr::get($params, 'variation');
36 57 $variation = CartHelper::normalizeCustomFields($variation);
37 58 }
38 59 else {
60 + // product_detail must be eager-loaded — generateCartItemFromVariation
61 + // reads $variation['product_detail']['variation_type'] to stamp
62 + // the cart item's variation_type. Without it the field is null on
63 + // instant-checkout carts and CartRenderer can't tell whether to
64 + // hide the variant-title line for simple products.
39 65 $variation = ProductVariation::query()
40 - ->with(['product'])
41 - ->with(['media', 'shippingClass'])
66 + ->with(['product', 'product_detail', 'media', 'shippingClass'])
42 67 ->where('id', $variationId)->first();
43 68
44 69 $variation = apply_filters('fluent_cart/cart/item_modify', $variation, [
45 70 'item_id' => $variationId,
@@ -55,8 +80,16 @@
55 80 'variation' => $variation,
56 81 'product' => !$isCustom ? $variation->product : []
57 82 ]);
58 83
84 + // After the filter, not before: this path takes its quantity straight from a
85 + // public URL param, and the filter above can replace it with anything.
86 + $error = CartHelper::validateQuantity($quantity);
87 + if ($error) {
88 + return $error;
89 + }
90 + $quantity = (int)$quantity;
91 +
59 92 if ($variation->payment_type === 'subscription') {
60 93 $quantity = 1;
61 94 }
62 95
@@ -86,11 +119,28 @@
86 119 if(!$isCustom) {
87 120 $cart = CartHelper::generateCartFromVariation($variation, $quantity);
88 121 }
89 122 else {
90 - // TODO: Legacy object-to-array conversion. Kept for backward compatibility.
123 + // Legacy object-to-array conversion. Kept for backward compatibility.
91 124 $cart = CartHelper::generateCartFromCustomVariation(json_decode(json_encode($variation), true), $quantity);
92 125 }
126 + } else {
127 + // Refresh cart_data from the current variation on every instant hit.
128 + // The inputs (variationId + quantity) are deterministic URL params,
129 + // so regenerating is idempotent — and it picks up any fields that
130 + // were missing on a previously-created draft (variation_type for
131 + // advanced-variation rows, refreshed pricing, updated featured
132 + // media). Without this, a stale draft from before a code change
133 + // keeps rendering with its old shape.
134 + if (!$isCustom) {
135 + $cart->cart_data = [
136 + CartHelper::generateCartItemFromVariation($variation, $quantity)
137 + ];
138 + } else {
139 + $cart->cart_data = [
140 + CartHelper::generateCartItemCustomItem(json_decode(json_encode($variation), true), $quantity)
141 + ];
142 + }
93 143 }
94 144
95 145 if (is_user_logged_in()) {
96 146 $cart->user_id = get_current_user_id();
@@ -119,11 +169,10 @@
119 169 *
120 170 */
121 171 public static function get(array $params = [])
122 172 {
123 - static $cart;
124 - if (isset($cart)) {
125 - return $cart;
173 + if (static::$cartCache !== false && static::$cartCache !== null) {
174 + return static::$cartCache;
126 175 }
127 176
128 177 $autoCreate = Arr::get($params, 'create', false);
129 178
@@ -138,9 +187,9 @@
138 187 ->where('cart_group', 'instant');
139 188
140 189 $tempCart = $cartQuery->first();
141 190
142 - $cart = $tempCart;
191 + static::$cartCache = $tempCart;
143 192
144 193 if (!$autoCreate) {
145 194 return $tempCart;
146 195 }
@@ -145,11 +194,11 @@
145 194 return $tempCart;
146 195 }
147 196 }
148 197
149 - $cart = static::getOrSetCartForThisDevice($autoCreate);
198 + static::$cartCache = static::getOrSetCartForThisDevice($autoCreate);
150 199
151 - return $cart;
200 + return static::$cartCache;
152 201 }
153 202
154 203 public static function find($id, $params = [])
155 204 {
@@ -174,11 +223,13 @@
174 223 {
175 224 $itemId = Arr::get($data, 'id');
176 225 $quantity = Arr::get($data, 'quantity', 1);
177 226
178 - if ($quantity <= 0) {
227 + // This path writes cart_data directly instead of going through Cart::addItem().
228 + $error = CartHelper::validateQuantity($quantity);
229 + if ($error) {
179 230 return static::makeErrorResponse([
180 - ['code' => 403, 'message' => __('Quantity can not be negative.', 'fluent-cart')]
231 + ['code' => 403, 'message' => $error->get_error_message()]
181 232 ]);
182 233 }
183 234
184 235 $cart = CartResource::get([
@@ -310,9 +361,21 @@
310 361 ]);
311 362 }
312 363
313 364 if (!$variation) {
314 - return $cart->removeItem($itemId);
365 + // An item already in the cart whose variation row has since
366 + // disappeared (product/variation deleted) is dropped gracefully.
367 + // An id that was never in the cart and resolves to nothing is a
368 + // client error — silently answering "Cart updated successfully"
369 + // hid typos and probing as a 200 no-op.
370 + if ($existingItem !== null) {
371 + return $cart->removeItem($itemId);
372 + }
373 +
374 + return new WP_Error(
375 + 'invalid_item',
376 + __('Invalid item.', 'fluent-cart')
377 + );
315 378 }
316 379
317 380 $soldIndividually = $isCustom
318 381 ? !empty($variation->sold_individually)
@@ -351,9 +414,13 @@
351 414 }
352 415
353 416 $utmData = static::prepareUtmData($data);
354 417 if ($utmData) {
355 - $cart->utm_data = array_merge(is_array($cart->utm_data) ? $cart->utm_data : [], $utmData);
418 + // Replaced, not merged. A cart row is reused across visits, so merging
419 + // key by key accumulated a union of every touch that ever reached it and
420 + // the column stopped describing any single one. The browser has already
421 + // resolved which touch this is, so its block is the answer.
422 + $cart->utm_data = $utmData;
356 423 $cart->save();
357 424 }
358 425
359 426 return $cart;
@@ -524,8 +591,15 @@
524 591 if ($updatedQuantity < 0) {
525 592 $updatedQuantity = 0;
526 593 }
527 594
595 + if ($updatedQuantity > 0 && ($error = CartHelper::validateQuantity($updatedQuantity))) {
596 + return [
597 + 'code' => 'failed',
598 + 'message' => $error->get_error_message()
599 + ];
600 + }
601 +
528 602 if (!$isFilteredItem) {
529 603
530 604 if (!CartHelper::shouldAddItemToCart($productVariation, $updatedQuantity)) {
531 605 return [
@@ -554,8 +628,16 @@
554 628
555 629 if ($quantity < 1) {
556 630 $quantity = 1;
557 631 }
632 +
633 + if ($error = CartHelper::validateQuantity($quantity)) {
634 + return [
635 + 'code' => 'failed',
636 + 'message' => $error->get_error_message()
637 + ];
638 + }
639 +
558 640 if (!$isFilteredItem) {
559 641 if (!CartHelper::shouldAddItemToCart($productVariation, $quantity)) {
560 642 return [
561 643 'code' => 'failed',
@@ -721,12 +803,15 @@
721 803 }
722 804
723 805 $userId = get_current_user_id();
724 806 if ($userId) {
807 + // Latest cart first — without an order, first() picks by primary key
808 + // (cart_hash), which resurrects an arbitrary old cart for the user.
725 809 $cart = static::getQuery()
726 810 ->where('user_id', $userId)
727 811 ->where('stage', '!=', 'completed')
728 812 ->where('cart_group', 'global')
813 + ->orderBy('updated_at', 'DESC')
729 814 ->first();
730 815
731 816 if ($cart) {
732 817 return $cart;