PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.16
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 1.9.13 All 162 releases
woocommerce-pos / includes / Sync / Product_Serializer.php

Product_Serializer.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.16, at includes/Sync/Product_Serializer.php

231 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WCPOS sync product serializer.
4 *
5 * @package WCPOS\WooCommercePOS\Sync
6 */
7
8 namespace WCPOS\WooCommercePOS\Sync;
9
10 use WC_Product;
11 use WC_Product_Variation;
12 use WC_REST_Product_Variations_Controller;
13 use WC_REST_Products_Controller;
14 use WP_REST_Request;
15
16 /**
17 * THE product-record assembly line.
18 *
19 * Every sync surface that needs a single product (or variation) document builds
20 * it here: the changes revision-hash walk, the barcode resolver, the targeted
21 * variations read and the write acknowledgement. Before this class each of those
22 * pasted the same four lines — controller instantiation, `prepare_object_for_response`,
23 * `rest_get_server()->response_to_data()`, then the augmentation filter — and the
24 * copies drifted (see the namespace-resolution comment the variations controller
25 * used to carry).
26 *
27 * Two rules the pasted blocks encoded and this class keeps:
28 *
29 * 1. ADR 0003 — values come from the FILTERED WC REST representation, never a raw
30 * projection. `prepare_object_for_response` runs WooCommerce's own
31 * `woocommerce_rest_prepare_product_object` filter; `response_to_data` resolves
32 * embedded links exactly as a real request would.
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.
40 */
41 final class Product_Serializer {
42 /**
43 * The WooCommerce products controller, created on first use.
44 *
45 * Memoized because the callers serialize in LOOPS (a revision-hash page, an
46 * include-set of variations); instantiating a controller per record was never
47 * the intent of the pasted blocks — each of them hoisted it out of the loop.
48 *
49 * @var null|WC_REST_Products_Controller
50 */
51 private $controller = null;
52
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 /**
69 * The request used when a caller does not supply one.
70 *
71 * @var null|WP_REST_Request
72 */
73 private $default_request = null;
74
75 /**
76 * Serialize one product or variation into its augmented REST representation.
77 *
78 * @param int|WC_Product $product Product id, or an already-loaded product/variation object.
79 * @param null|WP_REST_Request $request Serialization context. A bare `GET /` request is used when omitted.
80 *
81 * @return array The augmented payload, or an empty array when the id does not resolve to a product.
82 */
83 public function serialize( $product, ?WP_REST_Request $request = null ): array {
84 $object = $product instanceof WC_Product
85 ? $product
86 : ( \function_exists( 'wc_get_product' ) ? wc_get_product( (int) $product ) : false );
87
88 if ( ! $object instanceof WC_Product ) {
89 return array();
90 }
91
92 $request = $request instanceof WP_REST_Request ? $request : $this->default_request();
93 // Every lane that hydrates a product — changes, resolve, targeted
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
98 // `woocommerce_rest_prepare_product_object`. Without it the assembly line
99 // serializes the global price and the till redisplays it moments after the
100 // cashier changed the store's (pro#425). Stamping is idempotent and never
101 // overrides a scope the caller set deliberately.
102 Store_Scope::stamp( $request );
103 // Ours for the duration of the serialization — this runs outside any
104 // dispatch, so the lane marker is the only signal a response filter has.
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 ) );
120 }
121 );
122 /**
123 * WordPress response data is not guaranteed to be an array at runtime.
124 *
125 * @var mixed $payload
126 */
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 }
131
132 return self::augment( \is_array( $payload ) ? $payload : array(), $object, $request );
133 }
134
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 /**
164 * Run the public augmentation filter over an already-serialized payload.
165 *
166 * Exposed separately for the write acknowledgement, which already holds the
167 * bare wc/v3 data (it must hash the bare bytes for the conflict check) and only
168 * needs the stamps applied on top.
169 *
170 * @param array $payload Serialized product payload.
171 * @param mixed $object The product/variation backing the payload.
172 * @param null|WP_REST_Request $request Serialization context.
173 */
174 public static function augment( array $payload, $object = null, ?WP_REST_Request $request = null ): array {
175 if ( $request instanceof WP_REST_Request ) {
176 Store_Scope::stamp( $request );
177 }
178
179 /**
180 * Filters a serialized WCPOS product record.
181 *
182 * Additive only: it must never remove WooCommerce REST fields.
183 *
184 * @param array $payload Serialized product payload.
185 * @param mixed $object The product or variation backing the payload.
186 * @param null|WP_REST_Request $request Serialization context.
187 */
188 /**
189 * Public filters can return values outside the documented contract.
190 *
191 * @var mixed $augmented
192 */
193 $augmented = apply_filters( 'woocommerce_pos_sync_serialized_product', $payload, $object, $request );
194
195 return \is_array( $augmented ) ? $augmented : $payload;
196 }
197
198 /**
199 * The memoized WooCommerce products controller.
200 */
201 private function controller(): WC_REST_Products_Controller {
202 if ( null === $this->controller ) {
203 $this->controller = new WC_REST_Products_Controller();
204 }
205
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;
218 }
219
220 /**
221 * The memoized fallback serialization request.
222 */
223 private function default_request(): WP_REST_Request {
224 if ( null === $this->default_request ) {
225 $this->default_request = new WP_REST_Request( 'GET', '/' );
226 }
227
228 return $this->default_request;
229 }
230 }
231