| @@ -34,11 +34,11 @@ | ||
| 34 | 34 | * collection yields an EMPTY plan whose `filter()` is the identity and whose |
| 35 | 35 | * `around()` merely runs its callable. That is the adoption mechanism: a collection |
| 36 | 36 | * can be routed through the module before it has any rows, and nothing changes. |
| 37 | 37 | * - `Collection_Rules_Plan::filter()` — the direct lane. Type-preserving clause |
| 38 | - * bodies; it NEVER touches global filter state. Legacy callbacks can still delegate | |
| 39 | - * to it; collection reads use `around()` for search and visibility. | |
| 40 | - * - `Collection_Rules_Plan::around()` — the scoped read lanes, and the ONLY install path. | |
| 38 | + * bodies; it NEVER touches global filter state, so a v1 controller keeps owning its | |
| 39 | + * own `add_filter` topology (Pro subclasses those callbacks). | |
| 40 | + * - `Collection_Rules_Plan::around()` — the proxy lane, and the ONLY install path. | |
| 41 | 41 | * Callbacks are installed, the forward runs, and every binding is unwound in reverse |
| 42 | 42 | * in a `finally`. |
| 43 | 43 | * - `orderby_enum()` / `collection_params()` — schema PROJECTIONS of the same rows, so |
| 44 | 44 | * the REST schema and the proxy's claim list cannot disagree with the clause logic. |
| @@ -78,30 +78,9 @@ | ||
| 78 | 78 | * @var string |
| 79 | 79 | */ |
| 80 | 80 | public const STORAGE_HPOS = 'hpos'; |
| 81 | 81 | |
| 82 | - /** Shared search bound; over-limit policies remain collection-specific. */ | |
| 83 | - public const SEARCH_TERM_CAP = 10; | |
| 84 | - | |
| 85 | 82 | /** |
| 86 | - * Split literal terms. Orders now also drop Unicode control characters between terms. | |
| 87 | - * | |
| 88 | - * Existing defaults: product phrase null uses WP_Query terms; orders use the supplied | |
| 89 | - * string; variation args/discovery default to '', and validation casts null to ''. | |
| 90 | - * Product phrases collapse over-cap terms; orders slice; v2 variations reject. | |
| 91 | - * Every product/variation lane uses this splitter; v1 collapses over-cap, v2 flat variations reject. | |
| 92 | - * Malformed UTF-8 yields an empty array, including offset-capture callers. | |
| 93 | - * | |
| 94 | - * @param string $search Search text. | |
| 95 | - * @param int $flags Split flags. | |
| 96 | - * @return array | |
| 97 | - */ | |
| 98 | - public static function search_terms( $search, $flags = PREG_SPLIT_NO_EMPTY ) { | |
| 99 | - $terms = preg_split( '/[\s\p{Z}\p{C}]+/u', $search, -1, $flags ); | |
| 100 | - return false === $terms ? array() : $terms; | |
| 101 | - } | |
| 102 | - | |
| 103 | - /** | |
| 104 | 83 | * Legacy storage — `wp_posts` plus `wp_postmeta`. |
| 105 | 84 | * |
| 106 | 85 | * @var string |
| 107 | 86 | */ |
| @@ -107,9 +86,9 @@ | ||
| 107 | 86 | */ |
| 108 | 87 | public const STORAGE_POSTS = 'posts'; |
| 109 | 88 | |
| 110 | 89 | /** |
| 111 | - * Memoized plans, keyed by collection, request identity/content, storage and param map. | |
| 90 | + * Memoized plans, keyed by collection, request identity, storage and param map. | |
| 112 | 91 | * |
| 113 | 92 | * Each entry is `array( WP_REST_Request, Collection_Rules_Plan )`; the request is |
| 114 | 93 | * kept so a recycled `spl_object_id` can never serve another request's plan. |
| 115 | 94 | * |
| @@ -138,9 +117,9 @@ | ||
| 138 | 117 | * @return Collection_Rules_Plan |
| 139 | 118 | */ |
| 140 | 119 | public static function for_request( string $collection, WP_REST_Request $request, array $param_map = array(), ?string $storage = null ) { |
| 141 | 120 | $storage = $storage ?? self::detect_storage( $collection ); |
| 142 | - $key = $collection . '|' . spl_object_id( $request ) . '|' . $storage . '|' . md5( (string) wp_json_encode( array( $param_map, $request->get_route(), $request->get_params() ) ) ); | |
| 121 | + $key = $collection . '|' . spl_object_id( $request ) . '|' . $storage . '|' . md5( (string) wp_json_encode( $param_map ) ); | |
| 143 | 122 | |
| 144 | 123 | if ( isset( self::$plans[ $key ] ) && self::$plans[ $key ][0] === $request ) { |
| 145 | 124 | return self::$plans[ $key ][1]; |
| 146 | 125 | } |
| @@ -237,27 +216,13 @@ | ||
| 237 | 216 | * @internal |
| 238 | 217 | * |
| 239 | 218 | * @param string $collection Collection slug. |
| 240 | 219 | * |
| 241 | - * @return array{sorts?: array<string, array>, filters?: array<string, array>, search?: array<string, mixed>, visibility?: array<string, mixed>} | |
| 220 | + * @return array{sorts?: array<string, array>, filters?: array<string, array>} | |
| 242 | 221 | */ |
| 243 | 222 | public static function rules( string $collection ): array { |
| 244 | 223 | $rules = array( |
| 245 | 224 | 'orders' => array( |
| 246 | - 'search' => array( | |
| 247 | - 'param' => 'search', | |
| 248 | - 'term_cap' => self::SEARCH_TERM_CAP, | |
| 249 | - 'rank_exact' => false, | |
| 250 | - 'carriers' => array( 'id', 'billing_email', 'first_name', 'last_name', 'company', 'email', 'phone' ), | |
| 251 | - 'hpos' => array( | |
| 252 | - 'orders' => array( 'id', 'billing_email' ), | |
| 253 | - 'addresses' => array( 'first_name', 'last_name', 'company', 'email', 'phone' ), | |
| 254 | - ), | |
| 255 | - 'posts' => array( | |
| 256 | - 'id' => 'ID', | |
| 257 | - 'meta' => array( '_billing_first_name', '_billing_last_name', '_billing_company', '_billing_email', '_billing_phone' ), | |
| 258 | - ), | |
| 259 | - ), | |
| 260 | 225 | 'sorts' => array( |
| 261 | 226 | 'status' => array( |
| 262 | 227 | 'hpos' => array( 'column' => 'status' ), |
| 263 | 228 | 'posts' => array( 'posts_orderby' => 'post_status' ), |
| @@ -346,48 +311,11 @@ | ||
| 346 | 311 | * Neither collection is ever HPOS — both are posts on every store — so there is |
| 347 | 312 | * no `hpos` half to these rows. |
| 348 | 313 | */ |
| 349 | 314 | 'products' => array( |
| 350 | - 'search' => array( | |
| 351 | - 'param' => 'search', | |
| 352 | - // Both product lanes use the literal splitter and collapse over-cap terms to the phrase. | |
| 353 | - 'term_cap' => self::SEARCH_TERM_CAP, | |
| 354 | - 'rank_exact' => true, | |
| 355 | - 'carriers' => array_merge( array( 'post_title' ), Barcode_Field::search_keys() ), | |
| 356 | - 'posts' => array( 'meta' => Barcode_Field::search_keys() ), | |
| 357 | - 'hpos' => null, | |
| 358 | - ), | |
| 359 | - 'visibility' => array( | |
| 360 | - 'type' => array( | |
| 361 | - 'direct' => Pos_Visibility::PRODUCTS, | |
| 362 | - 'proxy' => Pos_Visibility::CATALOG, | |
| 363 | - ), | |
| 364 | - 'where_backstop' => true, | |
| 365 | - ), | |
| 366 | 315 | 'sorts' => self::catalog_meta_sorts(), |
| 367 | 316 | ), |
| 368 | 317 | 'variations' => array( |
| 369 | - 'search' => array( | |
| 370 | - 'param' => 'search', | |
| 371 | - 'term_cap' => self::SEARCH_TERM_CAP, | |
| 372 | - 'rank_exact' => false, | |
| 373 | - 'carriers' => Barcode_Field::search_keys(), | |
| 374 | - 'posts' => array( 'meta' => Barcode_Field::search_keys() ), | |
| 375 | - 'hpos' => null, | |
| 376 | - 'query' => 'meta_query', | |
| 377 | - 'exact_sku_param' => 'sku', | |
| 378 | - // Both lanes split literal terms; v1 keeps over-cap collapse/EXISTS, v2 rejects/meta_query. | |
| 379 | - 'lanes' => array( | |
| 380 | - 'direct' => array( | |
| 381 | - 'query' => 'wp_terms', | |
| 382 | - 'exact_sku_param' => null, | |
| 383 | - ), | |
| 384 | - ), | |
| 385 | - ), | |
| 386 | - 'visibility' => array( | |
| 387 | - 'type' => Pos_Visibility::VARIATIONS, | |
| 388 | - 'where_backstop' => true, | |
| 389 | - ), | |
| 390 | 318 | 'sorts' => self::catalog_meta_sorts(), |
| 391 | 319 | ), |
| 392 | 320 | |
| 393 | 321 | /* |
| @@ -474,9 +402,9 @@ | ||
| 474 | 402 | * @param string $collection Collection slug. |
| 475 | 403 | * |
| 476 | 404 | * @return string |
| 477 | 405 | */ |
| 478 | - public static function detect_storage( string $collection ): string { | |
| 406 | + private static function detect_storage( string $collection ): string { | |
| 479 | 407 | if ( 'orders' !== $collection ) { |
| 480 | 408 | return self::STORAGE_POSTS; |
| 481 | 409 | } |
| 482 | 410 | |