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

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

399 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WCPOS sync store component.
4 *
5 * @package WCPOS\WooCommercePOS\Sync
6 */
7
8 namespace WCPOS\WooCommercePOS\Sync;
9
10 // phpcs:disable Squiz.Commenting, Generic.Commenting -- Ported lab documentation is preserved verbatim.
11
12 /**
13 * THE collection registry (wayfinder #420 spec / #421 implementation): one
14 * table for the per-collection identity + capabilities that nine sites
15 * currently re-encode in three diverging vocabularies (plural collection,
16 * singular object_type, proxy resource slug). Consolidation precedent:
17 * trait-endpoint-permissions.php — but a trait shares BEHAVIOUR; this shares
18 * DATA, so it is a final class of pure lookups (no WP calls — unit-testable
19 * with no bootstrap, like the backfill controller's dispatchers it absorbs).
20 *
21 * Row shape — capabilities are NULLABLE GROUPS, never booleans: a capability
22 * a collection lacks is an ABSENT group, so "does X support Y" is
23 * `isset($row['y'])` and a group's fields exist only where they mean
24 * something (no empty-string taxonomy on a post collection, no id_type on
25 * tax_rates). Groups:
26 *
27 * - object_type — the singular journal vocabulary (always present).
28 * - identity — the uuid id-space: id_type (post|user|term|order), the
29 * collection's OWN scalar resolver scope (post_type or
30 * taxonomy — what resolve_id_by_uuid gets on a push; NOT
31 * the backfill scan scope), the ownership detector and the
32 * bulk uuid reader (METHOD NAMES on Pos_Uuid — rows stay
33 * pure data; consumers resolve callables), and the
34 * load_entity strategy key. null ⇒ no uuid identity
35 * (tax_rates, ADR 0009).
36 * - proxy — the namespaced read route + wc/v3 route + resource slug
37 * (the slug vocabulary trap: tax_rates' slug is `taxes`).
38 * - write — push support (the write map's route/id_type projection).
39 * - journal — the singular object_type the journal emits; orders consume
40 * it through their payload-windowed pull lane.
41 * - digest — leg-3 existence digests, present ONLY on the id-space
42 * OWNER row (products carries product+variation
43 * object_types; a copy on variations would double-project).
44 * Carries the stored object_types AND the live-row predicate
45 * name on Digest_Index, so the reader, the proxy stamper and
46 * the authoritative-absence answer all read one id-space fact
47 * instead of each re-deciding it.
48 * - repair — explicit drill_down/self_heal support, with a reason for nulls.
49 * - fingerprint — UNIVERSAL config-change detection membership; every
50 * collection carries it (null is invalid), with the barcode
51 * flag naming recipe membership. The contract-version lever
52 * itself lives in Config_Fingerprint::PAYLOAD_CONTRACT_VERSION,
53 * keyed by these same names.
54 * - backfill — uuid backfill support: the meta-store kind (post, order,
55 * user, or term) and the SCAN
56 * scope (products+variations scan together — which is why
57 * scan_post_types lives here and not on identity).
58 *
59 * Fail-closed lookups: unknown is null, NEVER a default. The
60 * default→products collapse in class-changes-controller.php:662 (a missing
61 * case pulls a PRODUCT with another record's id) is the bug class
62 * by_object_type() exists to kill — its consumers skip-and-log on null.
63 */
64 final class Collections {
65
66 /** One row per collection, keyed by the canonical plural name. */
67 private const ROWS = array(
68 'products' => array(
69 'object_type' => 'product',
70 'identity' => array(
71 'id_type' => 'post',
72 'post_type' => 'product',
73 'detector' => 'uuid_owned_by_other',
74 'bulk_reader' => 'bulk_read_post_uuids',
75 'loader' => 'product',
76 ),
77 'proxy' => array(
78 'route' => '/products',
79 'wc_route' => '/wc/v3/products',
80 'slug' => 'products',
81 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Products_Proxy_Behavior::class,
82 ),
83 'write' => array( 'route' => '/wc/v3/products' ),
84 'journal' => array( 'object_type' => 'product' ),
85 'digest' => array(
86 'id_space' => 'products',
87 'label' => '',
88 'object_types' => array( 'product', 'variation' ),
89 'child_type' => 'variation',
90 'select' => 'row_digest_select_sql',
91 'id_column' => 'p.ID',
92 'live_max' => 'product_live_max_id_sql',
93 'servable' => 'servable_product_ids',
94 'published_ids' => 'published_product_ids',
95 'live_rows' => 'live_row_exists_sql',
96 ),
97 'repair' => array(
98 'drill_down' => true,
99 'self_heal' => true,
100 'reason' => 'Products support drill-down and automatic repair, including variations in their shared id-space.',
101 ),
102 'fingerprint' => array( 'barcode' => true ),
103 'backfill' => array(
104 'kind' => 'post',
105 'scan_post_types' => array( 'product', 'product_variation' ),
106 ),
107 ),
108 'variations' => array(
109 'object_type' => 'variation',
110 'identity' => array(
111 'id_type' => 'post',
112 'post_type' => 'product_variation',
113 'detector' => 'uuid_owned_by_other',
114 // No bulk reader: variations are stamped through the
115 // serialized-product filter, not a proxy list page.
116 'bulk_reader' => null,
117 'loader' => 'product',
118 ),
119 // No wcpos proxy lane, and that is principled (ADR 0034): the flat
120 // /variations route is both the per-id hydration lane AND the
121 // list/seed lane (bare collection pages for the idle trickle).
122 'proxy' => null,
123 // Variations use WooCommerce's nested REST resource. The write controller
124 // takes the parent from create payloads and the stored object thereafter.
125 'write' => array( 'route' => '/wc/v3/products' ),
126 'journal' => array( 'object_type' => 'variation' ),
127 'digest' => null, // folded into the products id-space (owner row carries it)
128 'repair' => array(
129 'drill_down' => null,
130 'self_heal' => null,
131 'reason' => 'Variations are repaired through the products id-space.',
132 ),
133 'fingerprint' => array( 'barcode' => true ),
134 'backfill' => array(
135 'kind' => 'post',
136 'scan_post_types' => array( 'product', 'product_variation' ),
137 ),
138 ),
139 'orders' => array(
140 'object_type' => 'order',
141 'identity' => array(
142 'id_type' => 'order',
143 'detector' => 'uuid_owned_by_other_order',
144 'bulk_reader' => null, // payload mode — uuid read from served meta_data
145 'loader' => 'order',
146 ),
147 'proxy' => array(
148 'route' => '/orders',
149 'wc_route' => '/wc/v3/orders',
150 'slug' => 'orders',
151 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Orders_Proxy_Behavior::class,
152 ),
153 'write' => array( 'route' => '/wc/v3/orders' ),
154 'journal' => array( 'object_type' => 'order' ), // orders consume the journal via the payload-windowed pull lane, catalogue via the pointer stream
155 'digest' => array(
156 'id_space' => 'orders',
157 'label' => 'order ',
158 'object_types' => array( 'order' ),
159 'select' => 'order_digest_select_sql',
160 'id_column' => '{id}',
161 'live_max' => 'order_live_max_id_sql',
162 'live_rows' => 'order_live_row_exists_sql',
163 ),
164 'repair' => array(
165 'drill_down' => null,
166 'self_heal' => null,
167 'reason' => 'Detection is available; drill-down and automatic repair are not implemented.',
168 ),
169 'fingerprint' => array( 'barcode' => false ),
170 'backfill' => array( 'kind' => 'order' ),
171 ),
172 'customers' => array(
173 'object_type' => 'customer',
174 'identity' => array(
175 'id_type' => 'user',
176 'detector' => 'uuid_owned_by_other_user',
177 'bulk_reader' => 'bulk_read_user_uuids',
178 'loader' => 'customer',
179 ),
180 'proxy' => array(
181 'route' => '/customers',
182 'wc_route' => '/wc/v3/customers',
183 'slug' => 'customers',
184 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Customers_Proxy_Behavior::class,
185 ),
186 'write' => array( 'route' => '/wc/v3/customers' ),
187 'journal' => array( 'object_type' => 'customer' ),
188 'digest' => array(
189 'id_space' => 'customers',
190 'label' => 'customer ',
191 'object_types' => array( 'customer' ),
192 'select' => 'customer_digest_select_sql',
193 'id_column' => 'u.ID',
194 'live_max' => 'customer_live_max_id_sql',
195 'live_rows' => 'customer_live_row_exists_sql',
196 ),
197 'repair' => array(
198 'drill_down' => null,
199 'self_heal' => null,
200 'reason' => 'Detection is available; drill-down and automatic repair are not implemented.',
201 ),
202 'fingerprint' => array( 'barcode' => false ),
203 'backfill' => array( 'kind' => 'user' ),
204 ),
205 'categories' => array(
206 'object_type' => 'category',
207 'identity' => array(
208 'id_type' => 'term',
209 'taxonomy' => 'product_cat',
210 'detector' => 'uuid_owned_by_other_term',
211 'bulk_reader' => 'bulk_read_term_uuids',
212 'loader' => 'term',
213 ),
214 'proxy' => array(
215 'route' => '/products/categories',
216 'wc_route' => '/wc/v3/products/categories',
217 'slug' => 'categories',
218 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Terms_Proxy_Behavior::class,
219 ),
220 'write' => array( 'route' => '/wc/v3/products/categories' ),
221 'journal' => array( 'object_type' => 'category' ),
222 'digest' => null,
223 'repair' => array(
224 'drill_down' => null,
225 'self_heal' => null,
226 'reason' => 'No digest id-space is implemented for this collection.',
227 ),
228 'fingerprint' => array( 'barcode' => false ),
229 'backfill' => array(
230 'kind' => 'term',
231 'taxonomy' => 'product_cat',
232 ),
233 ),
234 'brands' => array(
235 'object_type' => 'brand',
236 'identity' => array(
237 'id_type' => 'term',
238 'taxonomy' => 'product_brand',
239 'detector' => 'uuid_owned_by_other_term',
240 'bulk_reader' => 'bulk_read_term_uuids',
241 'loader' => 'term',
242 ),
243 'proxy' => array(
244 'route' => '/products/brands',
245 'wc_route' => '/wc/v3/products/brands',
246 'slug' => 'brands',
247 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Terms_Proxy_Behavior::class,
248 ),
249 'write' => array( 'route' => '/wc/v3/products/brands' ),
250 'journal' => array( 'object_type' => 'brand' ),
251 'digest' => null,
252 'repair' => array(
253 'drill_down' => null,
254 'self_heal' => null,
255 'reason' => 'No digest id-space is implemented for this collection.',
256 ),
257 'fingerprint' => array( 'barcode' => false ),
258 'backfill' => array(
259 'kind' => 'term',
260 'taxonomy' => 'product_brand',
261 ),
262 ),
263 'tags' => array(
264 'object_type' => 'tag',
265 'identity' => array(
266 'id_type' => 'term',
267 'taxonomy' => 'product_tag',
268 'detector' => 'uuid_owned_by_other_term',
269 'bulk_reader' => 'bulk_read_term_uuids',
270 'loader' => 'term',
271 ),
272 'proxy' => array(
273 'route' => '/products/tags',
274 'wc_route' => '/wc/v3/products/tags',
275 'slug' => 'tags',
276 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Terms_Proxy_Behavior::class,
277 ),
278 'write' => null, // read-only: no client push path exists
279 'journal' => array( 'object_type' => 'tag' ),
280 'digest' => null,
281 'repair' => array(
282 'drill_down' => null,
283 'self_heal' => null,
284 'reason' => 'No digest id-space is implemented for this collection.',
285 ),
286 'fingerprint' => array( 'barcode' => false ),
287 'backfill' => array(
288 'kind' => 'term',
289 'taxonomy' => 'product_tag',
290 ),
291 ),
292 'coupons' => array(
293 'object_type' => 'coupon',
294 'identity' => array(
295 'id_type' => 'post',
296 'post_type' => 'shop_coupon',
297 'detector' => 'uuid_owned_by_other',
298 'bulk_reader' => 'bulk_read_post_uuids',
299 'loader' => 'coupon',
300 ),
301 'proxy' => array(
302 'route' => '/coupons',
303 'wc_route' => '/wc/v3/coupons',
304 'slug' => 'coupons',
305 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Coupons_Proxy_Behavior::class,
306 ),
307 'write' => array( 'route' => '/wc/v3/coupons' ),
308 'journal' => array( 'object_type' => 'coupon' ),
309 'digest' => null,
310 'repair' => array(
311 'drill_down' => null,
312 'self_heal' => null,
313 'reason' => 'No digest id-space is implemented for this collection.',
314 ),
315 'fingerprint' => array( 'barcode' => false ),
316 'backfill' => array(
317 'kind' => 'post',
318 'scan_post_types' => array( 'shop_coupon' ),
319 ),
320 ),
321 'tax_rates' => array(
322 'object_type' => 'tax_rate',
323 'identity' => null, // ADR 0009: keyed by WooCommerce id — no uuid identity, principled
324 'proxy' => array(
325 'route' => '/taxes',
326 'wc_route' => '/wc/v3/taxes',
327 'slug' => 'taxes',
328 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Taxes_Proxy_Behavior::class,
329 ),
330 'write' => null, // principled read-only
331 'journal' => array( 'object_type' => 'tax_rate' ),
332 'digest' => null,
333 'repair' => array(
334 'drill_down' => null,
335 'self_heal' => null,
336 'reason' => 'No digest id-space is implemented for this collection.',
337 ),
338 'fingerprint' => array( 'barcode' => false ),
339 'backfill' => null, // no meta store to stamp
340 ),
341 );
342
343 /** Row lookup by canonical plural name. Null for unknown — callers decide
344 * their unsupported behavior EXPLICITLY (no default-to-products, ever). */
345 public static function row( string $collection ): ?array {
346 return self::ROWS[ $collection ] ?? null;
347 }
348
349 /** Inverse lookup: singular change-log object_type → row (+ its plural name
350 * under '_collection'). `product` resolves EXPLICITLY — it is not a
351 * fall-through default anywhere in this class. */
352 public static function by_object_type( string $object_type ): ?array {
353 foreach ( self::ROWS as $collection => $row ) {
354 if ( $row['object_type'] === $object_type ) {
355 return array( '_collection' => $collection ) + $row;
356 }
357 }
358 return null;
359 }
360
361 /** Resolve a singular object type to its canonical collection name. */
362 public static function collection_for_object_type( string $object_type ): ?string {
363 $row = self::by_object_type( $object_type );
364
365 return null === $row ? null : $row['_collection'];
366 }
367
368 /** Inverse lookup: proxy resource slug → row (tax_rates' slug is `taxes`). */
369 public static function by_proxy_slug( string $slug ): ?array {
370 foreach ( self::ROWS as $collection => $row ) {
371 if ( isset( $row['proxy'] ) && $row['proxy']['slug'] === $slug ) {
372 return array( '_collection' => $collection ) + $row;
373 }
374 }
375 return null;
376 }
377
378 /**
379 * Capability projection: every row carrying a non-null `$capability`
380 * group, keyed by plural name. The write map, the proxy RESOURCES, the
381 * digest dispatch and plugin.php's stamper wiring are all projections of
382 * this — adding a collection means adding ONE row above.
383 */
384 public static function with( string $capability ): array {
385 $rows = array();
386 foreach ( self::ROWS as $collection => $row ) {
387 if ( isset( $row[ $capability ] ) ) {
388 $rows[ $collection ] = $row;
389 }
390 }
391 return $rows;
392 }
393
394 /** The full canonical name list (tests, docs, admin surfaces). */
395 public static function names(): array {
396 return array_keys( self::ROWS );
397 }
398 }
399