PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
← All changes | includes/Sync/Product_Serializer.php +85 -8 1.10.01.10.18 View file →
@@ -7,8 +7,10 @@
7 7
8 8 namespace WCPOS\WooCommercePOS\Sync;
9 9
10 10 use WC_Product;
11 +use WC_Product_Variation;
12 +use WC_REST_Product_Variations_Controller;
11 13 use WC_REST_Products_Controller;
12 14 use WP_REST_Request;
13 15
14 16 /**
@@ -27,11 +29,15 @@
27 29 * 1. ADR 0003 — values come from the FILTERED WC REST representation, never a raw
28 30 * projection. `prepare_object_for_response` runs WooCommerce's own
29 31 * `woocommerce_rest_prepare_product_object` filter; `response_to_data` resolves
30 32 * embedded links exactly as a real request would.
31 - * 2. Variations are serialized through the SAME products controller as products —
32 - * `wc_get_product()` hands back a `WC_Product_Variation` and the controller
33 - * handles it, so the two lanes cannot drift apart.
33 + * 2. Variations are serialized through WooCommerce's own VARIATIONS controller
34 + * (`WC_REST_Product_Variations_Controller`), products through the products
35 + * controller — this class picks per object type so a caller cannot pick
36 + * wrong. Hydrating a variation through the PRODUCTS controller is the #1710
37 + * incident (`images[]` instead of `image`: blank POS thumbnails, the
38 + * parent's image on every order line); post-#1710 the payload species is
39 + * the variations controller's everywhere.
34 40 */
35 41 final class Product_Serializer {
36 42 /**
37 43 * The WooCommerce products controller, created on first use.
@@ -44,8 +50,23 @@
44 50 */
45 51 private $controller = null;
46 52
47 53 /**
54 + * The WooCommerce product VARIATIONS controller, created on first use.
55 + *
56 + * A variation is not a product. WooCommerce serves it from its own controller, whose
57 + * response carries `image` (singular), `wc_get_formatted_variation()` as the name, and
58 + * none of the ~25 product-only fields (`categories`, `related_ids`, `price_html`, …)
59 + * that mean nothing on a variation. 1.9.x served exactly that shape from
60 + * `API\V1\Product_Variations_Controller`; hydrating variations through the PRODUCTS
61 + * controller instead is what dropped `image` and blanked every variation thumbnail in
62 + * the POS on 1.10.0 (#1710).
63 + *
64 + * @var null|WC_REST_Product_Variations_Controller
65 + */
66 + private $variations_controller = null;
67 +
68 + /**
48 69 * The request used when a caller does not supply one.
49 70 *
50 71 * @var null|WP_REST_Request
51 72 */
@@ -69,10 +90,12 @@
69 90 }
70 91
71 92 $request = $request instanceof WP_REST_Request ? $request : $this->default_request();
72 93 // Every lane that hydrates a product — changes, resolve, targeted
73 - // variations, the write ack — builds a bare `GET /` and hands it here, so
74 - // this is the ONE place that has to carry the till's store scope into
94 + // variations, the write ack — hands this method a request it may write to
95 + // (a bare `GET /`, or a CLONE of the live request; never the dispatched
96 + // request itself, because the stamps below mutate it), so this is the ONE
97 + // place that has to carry the till's store scope into
75 98 // `woocommerce_rest_prepare_product_object`. Without it the assembly line
76 99 // serializes the global price and the till redisplays it moments after the
77 100 // cashier changed the store's (pro#425). Stamping is idempotent and never
78 101 // overrides a scope the caller set deliberately.
@@ -78,11 +101,23 @@
78 101 // overrides a scope the caller set deliberately.
79 102 Store_Scope::stamp( $request );
80 103 // Ours for the duration of the serialization — this runs outside any
81 104 // dispatch, so the lane marker is the only signal a response filter has.
82 - $response = Store_Scope::in_v2_lane(
83 - function () use ( $object, $request ) {
84 - return rest_ensure_response( $this->controller()->prepare_object_for_response( $object, $request ) );
105 + $is_variation = $object instanceof WC_Product_Variation;
106 + if ( $is_variation ) {
107 + // `prepare_links()` reads `$request['product_id']` to build the nested
108 + // `products/<parent>/variations/<id>` route. No request handed here carries
109 + // it (bare, or cloned from the FLAT route), so without this the links
110 + // would claim parent 0.
111 + $request->set_param( 'product_id', $object->get_parent_id() );
112 + }
113 + // Store scope is carried by the request + lane marker above, both controller-agnostic,
114 + // and Pro registers `bake_store_prices` on the product AND variation prepare filters —
115 + // so store-scoped prices ride either controller (pro#425).
116 + $controller = $is_variation ? $this->variations_controller() : $this->controller();
117 + $response = Store_Scope::in_v2_lane(
118 + function () use ( $controller, $object, $request ) {
119 + return rest_ensure_response( $controller->prepare_object_for_response( $object, $request ) );
85 120 }
86 121 );
87 122 /**
88 123 * WordPress response data is not guaranteed to be an array at runtime.
@@ -89,13 +124,44 @@
89 124 *
90 125 * @var mixed $payload
91 126 */
92 127 $payload = rest_get_server()->response_to_data( $response, false );
128 + if ( $is_variation && \is_array( $payload ) ) {
129 + $payload = self::backfill_pre_wc83_variation_fields( $payload, $object );
130 + }
93 131
94 132 return self::augment( \is_array( $payload ) ? $payload : array(), $object, $request );
95 133 }
96 134
97 135 /**
136 + * `name` and `parent_id` on WooCommerce older than 8.3.
137 + *
138 + * WooCommerce added both to the VARIATIONS controller's response in 8.3; the products
139 + * controller has always emitted them. So moving variations onto their own controller would
140 + * silently drop two client-required fields on WooCommerce 5.3–8.2 — and this plugin still
141 + * declares `WC requires at least: 5.3`. The client reads `payload.name` for the variation row
142 + * title and `parent_id` to resolve the parent after a scan.
143 + *
144 + * The same backfill the v1 lane has always carried
145 + * (`API\V1\Product_Variations_Controller::wcpos_variation_response`), for the same reason.
146 + *
147 + * @param array $payload Serialized variation payload.
148 + * @param WC_Product_Variation $object The variation backing it.
149 + */
150 + private static function backfill_pre_wc83_variation_fields( array $payload, $object ): array {
151 + if ( ! isset( $payload['parent_id'] ) ) {
152 + $payload['parent_id'] = $object->get_parent_id();
153 + }
154 + if ( ! isset( $payload['name'] ) ) {
155 + $payload['name'] = \function_exists( 'wc_get_formatted_variation' )
156 + ? wc_get_formatted_variation( $object, true, false, false )
157 + : '';
158 + }
159 +
160 + return $payload;
161 + }
162 +
163 + /**
98 164 * Run the public augmentation filter over an already-serialized payload.
99 165 *
100 166 * Exposed separately for the write acknowledgement, which already holds the
101 167 * bare wc/v3 data (it must hash the bare bytes for the conflict check) and only
@@ -137,8 +203,19 @@
137 203 $this->controller = new WC_REST_Products_Controller();
138 204 }
139 205
140 206 return $this->controller;
207 + }
208 +
209 + /**
210 + * The memoized WooCommerce product variations controller.
211 + */
212 + private function variations_controller(): WC_REST_Product_Variations_Controller {
213 + if ( null === $this->variations_controller ) {
214 + $this->variations_controller = new WC_REST_Product_Variations_Controller();
215 + }
216 +
217 + return $this->variations_controller;
141 218 }
142 219
143 220 /**
144 221 * The memoized fallback serialization request.