| 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_REST_Products_Controller; |
| 12 |
use WP_REST_Request; |
| 13 |
|
| 14 |
/** |
| 15 |
* THE product-record assembly line. |
| 16 |
* |
| 17 |
* Every sync surface that needs a single product (or variation) document builds |
| 18 |
* it here: the changes revision-hash walk, the barcode resolver, the targeted |
| 19 |
* variations read and the write acknowledgement. Before this class each of those |
| 20 |
* pasted the same four lines — controller instantiation, `prepare_object_for_response`, |
| 21 |
* `rest_get_server()->response_to_data()`, then the augmentation filter — and the |
| 22 |
* copies drifted (see the namespace-resolution comment the variations controller |
| 23 |
* used to carry). |
| 24 |
* |
| 25 |
* Two rules the pasted blocks encoded and this class keeps: |
| 26 |
* |
| 27 |
* 1. ADR 0003 — values come from the FILTERED WC REST representation, never a raw |
| 28 |
* projection. `prepare_object_for_response` runs WooCommerce's own |
| 29 |
* `woocommerce_rest_prepare_product_object` filter; `response_to_data` resolves |
| 30 |
* 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. |
| 34 |
*/ |
| 35 |
final class Product_Serializer { |
| 36 |
/** |
| 37 |
* The WooCommerce products controller, created on first use. |
| 38 |
* |
| 39 |
* Memoized because the callers serialize in LOOPS (a revision-hash page, an |
| 40 |
* include-set of variations); instantiating a controller per record was never |
| 41 |
* the intent of the pasted blocks — each of them hoisted it out of the loop. |
| 42 |
* |
| 43 |
* @var null|WC_REST_Products_Controller |
| 44 |
*/ |
| 45 |
private $controller = null; |
| 46 |
|
| 47 |
/** |
| 48 |
* The request used when a caller does not supply one. |
| 49 |
* |
| 50 |
* @var null|WP_REST_Request |
| 51 |
*/ |
| 52 |
private $default_request = null; |
| 53 |
|
| 54 |
/** |
| 55 |
* Serialize one product or variation into its augmented REST representation. |
| 56 |
* |
| 57 |
* @param int|WC_Product $product Product id, or an already-loaded product/variation object. |
| 58 |
* @param null|WP_REST_Request $request Serialization context. A bare `GET /` request is used when omitted. |
| 59 |
* |
| 60 |
* @return array The augmented payload, or an empty array when the id does not resolve to a product. |
| 61 |
*/ |
| 62 |
public function serialize( $product, ?WP_REST_Request $request = null ): array { |
| 63 |
$object = $product instanceof WC_Product |
| 64 |
? $product |
| 65 |
: ( \function_exists( 'wc_get_product' ) ? wc_get_product( (int) $product ) : false ); |
| 66 |
|
| 67 |
if ( ! $object instanceof WC_Product ) { |
| 68 |
return array(); |
| 69 |
} |
| 70 |
|
| 71 |
$request = $request instanceof WP_REST_Request ? $request : $this->default_request(); |
| 72 |
// 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 |
| 75 |
// `woocommerce_rest_prepare_product_object`. Without it the assembly line |
| 76 |
// serializes the global price and the till redisplays it moments after the |
| 77 |
// cashier changed the store's (pro#425). Stamping is idempotent and never |
| 78 |
// overrides a scope the caller set deliberately. |
| 79 |
Store_Scope::stamp( $request ); |
| 80 |
// Ours for the duration of the serialization — this runs outside any |
| 81 |
// 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 ) ); |
| 85 |
} |
| 86 |
); |
| 87 |
/** |
| 88 |
* WordPress response data is not guaranteed to be an array at runtime. |
| 89 |
* |
| 90 |
* @var mixed $payload |
| 91 |
*/ |
| 92 |
$payload = rest_get_server()->response_to_data( $response, false ); |
| 93 |
|
| 94 |
return self::augment( \is_array( $payload ) ? $payload : array(), $object, $request ); |
| 95 |
} |
| 96 |
|
| 97 |
/** |
| 98 |
* Run the public augmentation filter over an already-serialized payload. |
| 99 |
* |
| 100 |
* Exposed separately for the write acknowledgement, which already holds the |
| 101 |
* bare wc/v3 data (it must hash the bare bytes for the conflict check) and only |
| 102 |
* needs the stamps applied on top. |
| 103 |
* |
| 104 |
* @param array $payload Serialized product payload. |
| 105 |
* @param mixed $object The product/variation backing the payload. |
| 106 |
* @param null|WP_REST_Request $request Serialization context. |
| 107 |
*/ |
| 108 |
public static function augment( array $payload, $object = null, ?WP_REST_Request $request = null ): array { |
| 109 |
if ( $request instanceof WP_REST_Request ) { |
| 110 |
Store_Scope::stamp( $request ); |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* Filters a serialized WCPOS product record. |
| 115 |
* |
| 116 |
* Additive only: it must never remove WooCommerce REST fields. |
| 117 |
* |
| 118 |
* @param array $payload Serialized product payload. |
| 119 |
* @param mixed $object The product or variation backing the payload. |
| 120 |
* @param null|WP_REST_Request $request Serialization context. |
| 121 |
*/ |
| 122 |
/** |
| 123 |
* Public filters can return values outside the documented contract. |
| 124 |
* |
| 125 |
* @var mixed $augmented |
| 126 |
*/ |
| 127 |
$augmented = apply_filters( 'woocommerce_pos_sync_serialized_product', $payload, $object, $request ); |
| 128 |
|
| 129 |
return \is_array( $augmented ) ? $augmented : $payload; |
| 130 |
} |
| 131 |
|
| 132 |
/** |
| 133 |
* The memoized WooCommerce products controller. |
| 134 |
*/ |
| 135 |
private function controller(): WC_REST_Products_Controller { |
| 136 |
if ( null === $this->controller ) { |
| 137 |
$this->controller = new WC_REST_Products_Controller(); |
| 138 |
} |
| 139 |
|
| 140 |
return $this->controller; |
| 141 |
} |
| 142 |
|
| 143 |
/** |
| 144 |
* The memoized fallback serialization request. |
| 145 |
*/ |
| 146 |
private function default_request(): WP_REST_Request { |
| 147 |
if ( null === $this->default_request ) { |
| 148 |
$this->default_request = new WP_REST_Request( 'GET', '/' ); |
| 149 |
} |
| 150 |
|
| 151 |
return $this->default_request; |
| 152 |
} |
| 153 |
} |
| 154 |
|