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 / Augmentation_Pipeline.php

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

262 lines 9.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WCPOS sync augmentation pipeline.
4 *
5 * @package WCPOS\WooCommercePOS\Sync
6 */
7
8 namespace WCPOS\WooCommercePOS\Sync;
9
10 /**
11 * THE augmentation pipeline behind both sync read lanes.
12 *
13 * A served record reaches the client down one of two lanes:
14 *
15 * - the BATCH lane — the catalog proxy hands a whole wc/v3 LIST to the public
16 * `woocommerce_pos_sync_proxy_response` filter;
17 * - the PER-OBJECT lane — {@see Product_Serializer} hands ONE serialized product
18 * to the public `woocommerce_pos_sync_serialized_product` filter.
19 *
20 * Every augmentation used to be wired to BOTH filters by hand, so each stamper
21 * shipped as a pair of near-identical twins (`stamp_proxy_*` / `stamp_serialized_*`)
22 * and a fix to one silently missed the other. The pipeline is the single place an
23 * augmentation is declared; the two public filter names survive as thin
24 * PROJECTIONS of it, so third-party code that hooks either name keeps working at
25 * the priorities it always used.
26 *
27 * Two kinds of entry:
28 *
29 * - RECORD augmenters — `callable( array $payload, mixed $object, mixed $request ): array`.
30 * Declared ONCE and projected onto both lanes; on the batch lane the pipeline
31 * walks the list and hands each record over with a null `$object`, which is
32 * exactly the per-object lane's contract (an augmenter that needs the WC object
33 * resolves it lazily by id, so a page of simple products never pays for a load
34 * it does not use).
35 * - LANE augmenters — pinned to one lane because they must see the WHOLE batch.
36 * The uuid, revision and digest stampers bulk-read their meta store in ONE query
37 * per page; folding them into the record loop would be N queries per page.
38 *
39 * Priorities are preserved verbatim from the hand-wiring: the pipeline adds ONE
40 * projection callback per (lane, priority) group, so a record augmenter declared
41 * at 10 still runs at 10 relative to any external hooker.
42 *
43 * `Meta_Normalizer` is deliberately NOT registered here: it also normalizes the
44 * ORDER lane, so it keeps its own three-filter registrar and stays at priority 5,
45 * ahead of everything this pipeline installs.
46 */
47 final class Augmentation_Pipeline {
48 /**
49 * The batch lane's public filter name.
50 */
51 public const PROXY_FILTER = 'woocommerce_pos_sync_proxy_response';
52
53 /**
54 * The per-object product lane's public filter name.
55 */
56 public const SERIALIZED_FILTER = 'woocommerce_pos_sync_serialized_product';
57
58 /**
59 * Lane id for the catalog-proxy batch response.
60 */
61 public const LANE_PROXY = 'proxy';
62
63 /**
64 * Lane id for the per-object serialized product payload.
65 */
66 public const LANE_SERIALIZED = 'serialized';
67
68 /**
69 * Registered record augmenters.
70 *
71 * @var array<int, array{callback: callable, priority: int, lanes: string[], resource: string}>
72 */
73 private static $record_augmenters = array();
74
75 /**
76 * Projection callbacks added to the public filters, kept for uninstall.
77 *
78 * @var array<int, array{0: string, 1: callable, 2: int}>
79 */
80 private static $projections = array();
81
82 /**
83 * Declare a per-record augmenter.
84 *
85 * @param callable $callback callable( array $payload, mixed $object, mixed $request ): array.
86 * @param int $priority Filter priority, matching the lane's public filter.
87 * @param string[] $lanes Lanes this augmenter serves. Defaults to both.
88 * @param string $resource Batch-lane resource slug this augmenter serves.
89 */
90 public static function add_record_augmenter( callable $callback, int $priority = 10, array $lanes = array( self::LANE_PROXY, self::LANE_SERIALIZED ), string $resource = 'products' ): void {
91 self::$record_augmenters[] = array(
92 'callback' => $callback,
93 'priority' => $priority,
94 'lanes' => $lanes,
95 'resource' => $resource,
96 );
97 }
98
99 /**
100 * Wire the default WCPOS augmentations onto both public filters.
101 *
102 * Called once from {@see \WCPOS\WooCommercePOS\Init} when the sync feature is
103 * enabled and the schema latch is healthy.
104 */
105 public static function install(): void {
106 self::reset();
107
108 // Batch-lane-only augmenters. Each bulk-reads its meta store once per page,
109 // so it MUST see the whole list; each keeps its own registrar because the
110 // registrars are also the seams the tests and the ops endpoints drive.
111 // Revision runs at 9 — BEFORE uuid/digest augmentation — so the stamped
112 // bytes equal what the write path recomputes from a bare wc/v3 re-read.
113 // The digest registrar also owns the ORDER pull lane's stamper, so every
114 // served-record augmentation is wired from here and nowhere else.
115 Revision::register_proxy_stamps();
116 Proxy_Uuid_Stamper::register_proxy_stampers();
117 Integrity_Digest::register_proxy_digest_stampers();
118
119 // Per-object-lane-only identity stamp: the batch lane's identity twin is
120 // Proxy_Uuid_Stamper above, which bulk-reads instead of loading per record.
121 self::add_record_augmenter(
122 array( Pos_Uuid::class, 'stamp_serialized_record' ),
123 10,
124 array( self::LANE_SERIALIZED )
125 );
126
127 // Declared ONCE, projected onto both lanes.
128 self::add_record_augmenter( array( Variable_Prices::class, 'augment_record' ), 10 );
129 // After the revision stamper at 9, so narrowing the child list cannot perturb the parent's
130 // `_rxdb_revision` — the write path recomputes that from a bare wc/v3 re-read.
131 self::add_record_augmenter( array( Variable_Children::class, 'augment_record' ), 10 );
132 self::add_record_augmenter( array( Product_Images::class, 'augment_record' ), 10 );
133
134 self::wire();
135 }
136
137 /**
138 * Project the current registry onto the two public filters.
139 *
140 * Adds ONE projection callback per (lane, priority) group, replacing any
141 * projections a previous call installed. Call this after declaring extra
142 * augmenters with {@see add_record_augmenter}.
143 */
144 public static function wire(): void {
145 self::unwire();
146 foreach ( self::grouped( self::LANE_PROXY ) as $priority => $entries ) {
147 $callback = static function ( $data, $resource = '', $request = null ) use ( $entries ) {
148 return self::apply_to_list( $data, $resource, $request, $entries );
149 };
150 add_filter( self::PROXY_FILTER, $callback, $priority, 3 );
151 self::$projections[] = array( self::PROXY_FILTER, $callback, $priority );
152 }
153 foreach ( self::grouped( self::LANE_SERIALIZED ) as $priority => $entries ) {
154 $callback = static function ( $payload, $object = null, $request = null ) use ( $entries ) {
155 return self::apply_to_record( $payload, $object, $request, $entries );
156 };
157 add_filter( self::SERIALIZED_FILTER, $callback, $priority, 3 );
158 self::$projections[] = array( self::SERIALIZED_FILTER, $callback, $priority );
159 }
160 }
161
162 /**
163 * Remove every projection this pipeline installed and forget the registry.
164 *
165 * The batch-lane registrars own their own unregister seams, so callers that
166 * need a full teardown call those too.
167 */
168 public static function reset(): void {
169 self::unwire();
170 self::$record_augmenters = array();
171 }
172
173 /**
174 * Apply the per-object lane's record augmenters to ONE serialized payload.
175 *
176 * @param mixed $payload Serialized product payload.
177 * @param mixed $object Product or variation backing the payload.
178 * @param mixed $request Serialization context.
179 * @param array $entries Record augmenters to apply.
180 *
181 * @return mixed
182 */
183 private static function apply_to_record( $payload, $object, $request, array $entries ) {
184 if ( ! \is_array( $payload ) ) {
185 return $payload;
186 }
187 foreach ( $entries as $entry ) {
188 $result = ( $entry['callback'] )( $payload, $object, $request );
189 $payload = \is_array( $result ) ? $result : $payload;
190 }
191
192 return $payload;
193 }
194
195 /**
196 * Apply the batch lane's record augmenters across a proxied list.
197 *
198 * Entries are applied OUTERMOST — the whole list passes through one augmenter
199 * before the next — preserving the order the hand-wired twins ran in.
200 *
201 * @param mixed $data Proxied response list.
202 * @param mixed $resource Proxy resource slug.
203 * @param mixed $request Request context.
204 * @param array $entries Record augmenters to apply.
205 *
206 * @return mixed
207 */
208 private static function apply_to_list( $data, $resource, $request, array $entries ) {
209 if ( ! \is_array( $data ) ) {
210 return $data;
211 }
212 foreach ( $entries as $entry ) {
213 if ( $entry['resource'] !== $resource ) {
214 continue;
215 }
216 foreach ( $data as $index => $record ) {
217 if ( ! \is_array( $record ) ) {
218 continue;
219 }
220 // The batch lane has no loaded object: augmenters resolve it lazily
221 // by id, exactly as they do on the per-object lane.
222 $result = ( $entry['callback'] )( $record, null, $request );
223 if ( \is_array( $result ) ) {
224 $data[ $index ] = $result;
225 }
226 }
227 }
228
229 return $data;
230 }
231
232 /**
233 * Detach every projection callback this pipeline added, keeping the registry.
234 */
235 private static function unwire(): void {
236 foreach ( self::$projections as $projection ) {
237 remove_filter( $projection[0], $projection[1], $projection[2] );
238 }
239 self::$projections = array();
240 }
241
242 /**
243 * Group one lane's record augmenters by priority, ascending, registration-stable.
244 *
245 * @param string $lane Lane id.
246 *
247 * @return array<int, array<int, array{callback: callable, priority: int, lanes: string[], resource: string}>>
248 */
249 private static function grouped( string $lane ): array {
250 $groups = array();
251 foreach ( self::$record_augmenters as $entry ) {
252 if ( ! \in_array( $lane, $entry['lanes'], true ) ) {
253 continue;
254 }
255 $groups[ $entry['priority'] ][] = $entry;
256 }
257 ksort( $groups );
258
259 return $groups;
260 }
261 }
262