PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
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 1.9.12 1.9.11 1.9.10 1.9.9 All 158 releases
woocommerce-pos / includes / Sync / Collections.php

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

338 lines 13.0 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 * - fingerprint — UNIVERSAL config-change detection membership; every
49 * collection carries it (null is invalid), with the barcode
50 * flag naming recipe membership. The contract-version lever
51 * itself lives in Config_Fingerprint::PAYLOAD_CONTRACT_VERSION,
52 * keyed by these same names.
53 * - backfill — uuid backfill support: the meta-store kind (post, order,
54 * user, or term) and the SCAN
55 * scope (products+variations scan together — which is why
56 * scan_post_types lives here and not on identity).
57 *
58 * Fail-closed lookups: unknown is null, NEVER a default. The
59 * default→products collapse in class-changes-controller.php:662 (a missing
60 * case pulls a PRODUCT with another record's id) is the bug class
61 * by_object_type() exists to kill — its consumers skip-and-log on null.
62 */
63 final class Collections {
64
65 /** One row per collection, keyed by the canonical plural name. */
66 private const ROWS = array(
67 'products' => array(
68 'object_type' => 'product',
69 'identity' => array(
70 'id_type' => 'post',
71 'post_type' => 'product',
72 'detector' => 'uuid_owned_by_other',
73 'bulk_reader' => 'bulk_read_post_uuids',
74 'loader' => 'product',
75 ),
76 'proxy' => array(
77 'route' => '/products',
78 'wc_route' => '/wc/v3/products',
79 'slug' => 'products',
80 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Products_Proxy_Behavior::class,
81 ),
82 'write' => array( 'route' => '/wc/v3/products' ),
83 'journal' => array( 'object_type' => 'product' ),
84 'digest' => array(
85 'id_space' => 'products',
86 'object_types' => array( 'product', 'variation' ),
87 'live_rows' => 'live_row_exists_sql',
88 ),
89 'fingerprint' => array( 'barcode' => true ),
90 'backfill' => array(
91 'kind' => 'post',
92 'scan_post_types' => array( 'product', 'product_variation' ),
93 ),
94 ),
95 'variations' => array(
96 'object_type' => 'variation',
97 'identity' => array(
98 'id_type' => 'post',
99 'post_type' => 'product_variation',
100 'detector' => 'uuid_owned_by_other',
101 // No bulk reader: variations are stamped through the
102 // serialized-product filter, not a proxy list page.
103 'bulk_reader' => null,
104 'loader' => 'product',
105 ),
106 // No wcpos proxy lane, and that is principled (ADR 0034): the flat
107 // /variations route is both the per-id hydration lane AND the
108 // list/seed lane (bare collection pages for the idle trickle).
109 'proxy' => null,
110 // Variations use WooCommerce's nested REST resource. The write controller
111 // takes the parent from create payloads and the stored object thereafter.
112 'write' => array( 'route' => '/wc/v3/products' ),
113 'journal' => array( 'object_type' => 'variation' ),
114 'digest' => null, // folded into the products id-space (owner row carries it)
115 'fingerprint' => array( 'barcode' => true ),
116 'backfill' => array(
117 'kind' => 'post',
118 'scan_post_types' => array( 'product', 'product_variation' ),
119 ),
120 ),
121 'orders' => array(
122 'object_type' => 'order',
123 'identity' => array(
124 'id_type' => 'order',
125 'detector' => 'uuid_owned_by_other_order',
126 'bulk_reader' => null, // payload mode — uuid read from served meta_data
127 'loader' => 'order',
128 ),
129 'proxy' => array(
130 'route' => '/orders',
131 'wc_route' => '/wc/v3/orders',
132 'slug' => 'orders',
133 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Orders_Proxy_Behavior::class,
134 ),
135 'write' => array( 'route' => '/wc/v3/orders' ),
136 'journal' => array( 'object_type' => 'order' ), // orders consume the journal via the payload-windowed pull lane, catalogue via the pointer stream
137 'digest' => array(
138 'id_space' => 'orders',
139 'object_types' => array( 'order' ),
140 'live_rows' => 'order_live_row_exists_sql',
141 ),
142 'fingerprint' => array( 'barcode' => false ),
143 'backfill' => array( 'kind' => 'order' ),
144 ),
145 'customers' => array(
146 'object_type' => 'customer',
147 'identity' => array(
148 'id_type' => 'user',
149 'detector' => 'uuid_owned_by_other_user',
150 'bulk_reader' => 'bulk_read_user_uuids',
151 'loader' => 'customer',
152 ),
153 'proxy' => array(
154 'route' => '/customers',
155 'wc_route' => '/wc/v3/customers',
156 'slug' => 'customers',
157 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Customers_Proxy_Behavior::class,
158 ),
159 'write' => array( 'route' => '/wc/v3/customers' ),
160 'journal' => array( 'object_type' => 'customer' ),
161 'digest' => array(
162 'id_space' => 'customers',
163 'object_types' => array( 'customer' ),
164 'live_rows' => 'customer_live_row_exists_sql',
165 ),
166 'fingerprint' => array( 'barcode' => false ),
167 'backfill' => array( 'kind' => 'user' ),
168 ),
169 'categories' => array(
170 'object_type' => 'category',
171 'identity' => array(
172 'id_type' => 'term',
173 'taxonomy' => 'product_cat',
174 'detector' => 'uuid_owned_by_other_term',
175 'bulk_reader' => 'bulk_read_term_uuids',
176 'loader' => 'term',
177 ),
178 'proxy' => array(
179 'route' => '/products/categories',
180 'wc_route' => '/wc/v3/products/categories',
181 'slug' => 'categories',
182 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Terms_Proxy_Behavior::class,
183 ),
184 'write' => array( 'route' => '/wc/v3/products/categories' ),
185 'journal' => array( 'object_type' => 'category' ),
186 'digest' => null,
187 'fingerprint' => array( 'barcode' => false ),
188 'backfill' => array(
189 'kind' => 'term',
190 'taxonomy' => 'product_cat',
191 ),
192 ),
193 'brands' => array(
194 'object_type' => 'brand',
195 'identity' => array(
196 'id_type' => 'term',
197 'taxonomy' => 'product_brand',
198 'detector' => 'uuid_owned_by_other_term',
199 'bulk_reader' => 'bulk_read_term_uuids',
200 'loader' => 'term',
201 ),
202 'proxy' => array(
203 'route' => '/products/brands',
204 'wc_route' => '/wc/v3/products/brands',
205 'slug' => 'brands',
206 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Terms_Proxy_Behavior::class,
207 ),
208 'write' => array( 'route' => '/wc/v3/products/brands' ),
209 'journal' => array( 'object_type' => 'brand' ),
210 'digest' => null,
211 'fingerprint' => array( 'barcode' => false ),
212 'backfill' => array(
213 'kind' => 'term',
214 'taxonomy' => 'product_brand',
215 ),
216 ),
217 'tags' => array(
218 'object_type' => 'tag',
219 'identity' => array(
220 'id_type' => 'term',
221 'taxonomy' => 'product_tag',
222 'detector' => 'uuid_owned_by_other_term',
223 'bulk_reader' => 'bulk_read_term_uuids',
224 'loader' => 'term',
225 ),
226 'proxy' => array(
227 'route' => '/products/tags',
228 'wc_route' => '/wc/v3/products/tags',
229 'slug' => 'tags',
230 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Terms_Proxy_Behavior::class,
231 ),
232 'write' => null, // read-only: no client push path exists
233 'journal' => array( 'object_type' => 'tag' ),
234 'digest' => null,
235 'fingerprint' => array( 'barcode' => false ),
236 'backfill' => array(
237 'kind' => 'term',
238 'taxonomy' => 'product_tag',
239 ),
240 ),
241 'coupons' => array(
242 'object_type' => 'coupon',
243 'identity' => array(
244 'id_type' => 'post',
245 'post_type' => 'shop_coupon',
246 'detector' => 'uuid_owned_by_other',
247 'bulk_reader' => 'bulk_read_post_uuids',
248 'loader' => 'coupon',
249 ),
250 'proxy' => array(
251 'route' => '/coupons',
252 'wc_route' => '/wc/v3/coupons',
253 'slug' => 'coupons',
254 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Coupons_Proxy_Behavior::class,
255 ),
256 'write' => array( 'route' => '/wc/v3/coupons' ),
257 'journal' => array( 'object_type' => 'coupon' ),
258 'digest' => null,
259 'fingerprint' => array( 'barcode' => false ),
260 'backfill' => array(
261 'kind' => 'post',
262 'scan_post_types' => array( 'shop_coupon' ),
263 ),
264 ),
265 'tax_rates' => array(
266 'object_type' => 'tax_rate',
267 'identity' => null, // ADR 0009: keyed by WooCommerce id — no uuid identity, principled
268 'proxy' => array(
269 'route' => '/taxes',
270 'wc_route' => '/wc/v3/taxes',
271 'slug' => 'taxes',
272 'behavior' => \WCPOS\WooCommercePOS\API\V2\Proxy\Taxes_Proxy_Behavior::class,
273 ),
274 'write' => null, // principled read-only
275 'journal' => array( 'object_type' => 'tax_rate' ),
276 'digest' => null,
277 'fingerprint' => array( 'barcode' => false ),
278 'backfill' => null, // no meta store to stamp
279 ),
280 );
281
282 /** Row lookup by canonical plural name. Null for unknown — callers decide
283 * their unsupported behavior EXPLICITLY (no default-to-products, ever). */
284 public static function row( string $collection ): ?array {
285 return self::ROWS[ $collection ] ?? null;
286 }
287
288 /** Inverse lookup: singular change-log object_type → row (+ its plural name
289 * under '_collection'). `product` resolves EXPLICITLY — it is not a
290 * fall-through default anywhere in this class. */
291 public static function by_object_type( string $object_type ): ?array {
292 foreach ( self::ROWS as $collection => $row ) {
293 if ( $row['object_type'] === $object_type ) {
294 return array( '_collection' => $collection ) + $row;
295 }
296 }
297 return null;
298 }
299
300 /** Resolve a singular object type to its canonical collection name. */
301 public static function collection_for_object_type( string $object_type ): ?string {
302 $row = self::by_object_type( $object_type );
303
304 return null === $row ? null : $row['_collection'];
305 }
306
307 /** Inverse lookup: proxy resource slug → row (tax_rates' slug is `taxes`). */
308 public static function by_proxy_slug( string $slug ): ?array {
309 foreach ( self::ROWS as $collection => $row ) {
310 if ( isset( $row['proxy'] ) && $row['proxy']['slug'] === $slug ) {
311 return array( '_collection' => $collection ) + $row;
312 }
313 }
314 return null;
315 }
316
317 /**
318 * Capability projection: every row carrying a non-null `$capability`
319 * group, keyed by plural name. The write map, the proxy RESOURCES, the
320 * digest dispatch and plugin.php's stamper wiring are all projections of
321 * this — adding a collection means adding ONE row above.
322 */
323 public static function with( string $capability ): array {
324 $rows = array();
325 foreach ( self::ROWS as $collection => $row ) {
326 if ( isset( $row[ $capability ] ) ) {
327 $rows[ $collection ] = $row;
328 }
329 }
330 return $rows;
331 }
332
333 /** The full canonical name list (tests, docs, admin surfaces). */
334 public static function names(): array {
335 return array_keys( self::ROWS );
336 }
337 }
338