| @@ -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. |