| @@ -27,12 +27,13 @@ | ||
| 27 | 27 | * set this plan has taken ownership of. |
| 28 | 28 | * |
| 29 | 29 | * # Two application modes |
| 30 | 30 | * |
| 31 | - * `filter()` also serves legacy callbacks (Pro subclasses them). This method never | |
| 32 | - * touches global filter state; search and visibility use the scoped `around()` path. | |
| 31 | + * `filter()` is the direct lane: the v1 controller keeps its own `add_filter` topology | |
| 32 | + * (Pro subclasses those callbacks) and each callback body hands its value here. This | |
| 33 | + * method never touches global filter state. | |
| 33 | 34 | * |
| 34 | - * `around()` is the scoped read path and the ONLY path that installs anything. Bindings are | |
| 35 | + * `around()` is the proxy lane and the ONLY path that installs anything. Bindings are | |
| 35 | 36 | * captured as closures — never re-derived tuples — installed, and unwound in reverse |
| 36 | 37 | * inside a `finally`, so a throwing forward leaves `$wp_filter` exactly as it found it. |
| 37 | 38 | * |
| 38 | 39 | * # The WooCommerce-owned sorts |
| @@ -157,29 +158,8 @@ | ||
| 157 | 158 | */ |
| 158 | 159 | private $request_order; |
| 159 | 160 | |
| 160 | 161 | /** |
| 161 | - * Literal search phrase. | |
| 162 | - * | |
| 163 | - * @var string | |
| 164 | - */ | |
| 165 | - private $search = ''; | |
| 166 | - | |
| 167 | - /** | |
| 168 | - * Exact SKU lookup, taking precedence over variation search. | |
| 169 | - * | |
| 170 | - * @var string | |
| 171 | - */ | |
| 172 | - private $sku = ''; | |
| 173 | - | |
| 174 | - /** | |
| 175 | - * The lane's declared visibility type. | |
| 176 | - * | |
| 177 | - * @var string|null | |
| 178 | - */ | |
| 179 | - private $visibility_type; | |
| 180 | - | |
| 181 | - /** | |
| 182 | 162 | * Build a plan. Use `Collection_Rules::for_request()`. |
| 183 | 163 | * |
| 184 | 164 | * @internal |
| 185 | 165 | * |
| @@ -191,10 +171,8 @@ | ||
| 191 | 171 | */ |
| 192 | 172 | public function __construct( string $collection, array $rules, string $storage, WP_REST_Request $request, array $param_map ) { |
| 193 | 173 | $this->collection = $collection; |
| 194 | 174 | $this->rules = $rules; |
| 195 | - $lane = 0 === strpos( $request->get_route(), '/wcpos/v1/' ) ? 'direct' : 'proxy'; | |
| 196 | - $this->rules['search'] = array_replace( $rules['search'] ?? array(), $rules['search']['lanes'][ $lane ] ?? array() ); | |
| 197 | 175 | $this->storage = $storage; |
| 198 | 176 | |
| 199 | 177 | $order_key = $this->request_key( $param_map, 'order' ); |
| 200 | 178 | $raw_order = null === $order_key ? null : $request->get_param( $order_key ); |
| @@ -201,29 +179,8 @@ | ||
| 201 | 179 | $this->request_order = \is_string( $raw_order ) && '' !== $raw_order ? $raw_order : null; |
| 202 | 180 | |
| 203 | 181 | $this->claim_sort( $request, $param_map ); |
| 204 | 182 | $this->claim_filters( $request, $param_map ); |
| 205 | - $key = $this->request_key( $param_map, $rules['search']['param'] ?? 'search' ); | |
| 206 | - $search = null === $key ? null : $request->get_param( $key ); | |
| 207 | - if ( isset( $rules['search'] ) && \is_string( $search ) && '' !== $search ) { | |
| 208 | - $terms = Collection_Rules::search_terms( $search ); | |
| 209 | - if ( 'orders' !== $collection || array() !== $terms || false === preg_match( '//u', $search ) ) { | |
| 210 | - $this->search = $search; | |
| 211 | - if ( 'products' === $collection && false !== preg_match( '//u', $search ) ) { | |
| 212 | - $parts = Collection_Rules::search_terms( $search, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE ); | |
| 213 | - $last = end( $parts ); | |
| 214 | - $this->search = array() === $parts ? '' : substr( $search, $parts[0][1], $last[1] + strlen( $last[0] ) - $parts[0][1] ); | |
| 215 | - } | |
| 216 | - $this->claims['search'] = $this->search; | |
| 217 | - $this->claimed_keys[] = $key; | |
| 218 | - } | |
| 219 | - } | |
| 220 | - $sku_param = $this->rules['search']['exact_sku_param'] ?? null; | |
| 221 | - if ( null !== $sku_param ) { | |
| 222 | - $this->sku = trim( (string) ( $request->get_param( $sku_param ) ?? '' ), " \t\n\r\0\x0B," ); | |
| 223 | - } | |
| 224 | - $type = $rules['visibility']['type'] ?? null; | |
| 225 | - $this->visibility_type = \is_array( $type ) ? $type[ $lane ] : $type; | |
| 226 | 183 | } |
| 227 | 184 | |
| 228 | 185 | /** |
| 229 | 186 | * The collection this plan was built for. |
| @@ -248,9 +205,9 @@ | ||
| 248 | 205 | * |
| 249 | 206 | * @return bool |
| 250 | 207 | */ |
| 251 | 208 | public function is_empty(): bool { |
| 252 | - return null === $this->sort && array() === $this->claims && null === $this->visibility_type; | |
| 209 | + return null === $this->sort && array() === $this->claims; | |
| 253 | 210 | } |
| 254 | 211 | |
| 255 | 212 | /** |
| 256 | 213 | * The canonical sort this plan owns, or null. |
| @@ -305,15 +262,9 @@ | ||
| 305 | 262 | * |
| 306 | 263 | * @return mixed |
| 307 | 264 | */ |
| 308 | 265 | public function filter( string $hook, $value, ...$context ) { |
| 309 | - $value = $this->apply_read_rule( $hook, $value, $context[0] ?? null ); | |
| 310 | 266 | switch ( $hook ) { |
| 311 | - case 'posts_search': | |
| 312 | - case 'posts_join': | |
| 313 | - case 'posts_groupby': | |
| 314 | - case 'search_orderby': | |
| 315 | - return $value; | |
| 316 | 267 | case self::HOOK_QUERY_ARGS: |
| 317 | 268 | return \is_array( $value ) ? $this->apply_meta_filters( $value ) : $value; |
| 318 | 269 | |
| 319 | 270 | case self::HOOK_PREPARE_ARGS: |
| @@ -352,9 +303,9 @@ | ||
| 352 | 303 | |
| 353 | 304 | /** |
| 354 | 305 | * Install this plan's callbacks, run `$run`, then unwind every binding in reverse. |
| 355 | 306 | * |
| 356 | - * The scoped read lanes' ONLY install path. Bindings are closures captured here, so the | |
| 307 | + * The proxy lane's ONLY install path. Bindings are closures captured here, so the | |
| 357 | 308 | * unwind removes the exact callables that were added — never a re-derived tuple that |
| 358 | 309 | * could miss. An exception from `$run` propagates AFTER the unwind. |
| 359 | 310 | * |
| 360 | 311 | * @param callable $run The forward to wrap. |
| @@ -431,49 +382,8 @@ | ||
| 431 | 382 | if ( $this->is_empty() ) { |
| 432 | 383 | return $bindings; |
| 433 | 384 | } |
| 434 | 385 | |
| 435 | - if ( null !== $this->visibility_type ) { | |
| 436 | - $hooks = array( | |
| 437 | - self::HOOK_POSTS_WHERE => 10, | |
| 438 | - self::HOOK_POSTS_CLAUSES => 10, | |
| 439 | - ); | |
| 440 | - if ( '' !== $this->search ) { | |
| 441 | - $hooks += array( | |
| 442 | - 'posts_search' => 10, | |
| 443 | - 'posts_join' => 10, | |
| 444 | - 'posts_groupby' => 10, | |
| 445 | - 'search_orderby' => 20, | |
| 446 | - ); | |
| 447 | - } | |
| 448 | - foreach ( $hooks as $role => $priority ) { | |
| 449 | - $hook = 'search_orderby' === $role ? 'posts_clauses' : $role; | |
| 450 | - $callback = function ( $value, $query ) use ( $role ) { | |
| 451 | - $type = 'variations' === $this->collection ? 'product_variation' : 'product'; | |
| 452 | - return in_array( $type, (array) ( $query->query_vars['post_type'] ?? null ), true ) | |
| 453 | - ? $this->filter( $role, $value, $query ) : $value; | |
| 454 | - }; | |
| 455 | - add_filter( $hook, $callback, $priority, 2 ); | |
| 456 | - $bindings[] = array( $hook, $callback, $priority ); | |
| 457 | - } | |
| 458 | - if ( 'products' === $this->collection ) { | |
| 459 | - $callback = function ( $args ) { | |
| 460 | - return $this->filter( self::HOOK_PREPARE_ARGS, $args ); | |
| 461 | - }; | |
| 462 | - add_filter( 'woocommerce_rest_product_object_query', $callback ); | |
| 463 | - $bindings[] = array( 'woocommerce_rest_product_object_query', $callback, 10 ); | |
| 464 | - } | |
| 465 | - if ( '' !== $this->search ) { | |
| 466 | - $vars = static function ( $vars ) { | |
| 467 | - $vars[] = 'wcpos_search_phrase'; | |
| 468 | - return $vars; | |
| 469 | - }; | |
| 470 | - add_filter( 'woocommerce_rest_query_vars', $vars ); | |
| 471 | - $bindings[] = array( 'woocommerce_rest_query_vars', $vars, 10 ); | |
| 472 | - } | |
| 473 | - return $bindings; | |
| 474 | - } | |
| 475 | - | |
| 476 | 386 | // `meta_query` rows are storage-neutral (`wc_get_orders()` honours them on both), |
| 477 | 387 | // and the legacy sort args are a no-op under HPOS, so one binding covers both. |
| 478 | 388 | if ( array() !== $this->claimed_meta_filters() || $this->has_legacy_meta_sort() ) { |
| 479 | 389 | $args_callback = function ( $args ) { |
| @@ -507,9 +417,9 @@ | ||
| 507 | 417 | add_filter( 'posts_orderby', $orderby_callback, 10, 2 ); |
| 508 | 418 | $bindings[] = array( 'posts_orderby', $orderby_callback, 10 ); |
| 509 | 419 | } |
| 510 | 420 | |
| 511 | - if ( $this->claims_id_sets() || '' !== $this->search ) { | |
| 421 | + if ( $this->claims_id_sets() ) { | |
| 512 | 422 | /* |
| 513 | 423 | * `posts_where` fires for EVERY WP_Query, and `wcpos/v1` leaves its callback |
| 514 | 424 | * installed for the remainder of the request without a post-type guard (frozen |
| 515 | 425 | * behaviour, reproduced verbatim in the clause body). The proxy lane scopes the |
| @@ -531,80 +441,8 @@ | ||
| 531 | 441 | $bindings[] = array( 'posts_where', $where_callback, 10 ); |
| 532 | 442 | } |
| 533 | 443 | |
| 534 | 444 | return $bindings; |
| 535 | - } | |
| 536 | - | |
| 537 | - /** | |
| 538 | - * Apply the declared search and visibility bodies without installing hooks. | |
| 539 | - * | |
| 540 | - * @param string $hook Clause role. | |
| 541 | - * @param mixed $value Value to filter. | |
| 542 | - * @param mixed $query Query instance. | |
| 543 | - * @return mixed | |
| 544 | - */ | |
| 545 | - private function apply_read_rule( string $hook, $value, $query ) { | |
| 546 | - global $wpdb; | |
| 547 | - $rule = $this->rules['search'] ?? array(); | |
| 548 | - $q = $query->query_vars ?? array(); | |
| 549 | - if ( self::HOOK_PREPARE_ARGS === $hook && \is_array( $value ) && null !== $this->visibility_type ) { | |
| 550 | - $value = ( new Pos_Visibility() )->apply_to_wp_query_args( $value, $this->collection, null, $this->visibility_type ); | |
| 551 | - if ( 'variations' === $this->collection && 'meta_query' === $rule['query'] ) { | |
| 552 | - return Product_Search::variation_args( $value, $this->search, $this->sku, $rule ); | |
| 553 | - } | |
| 554 | - if ( '' !== $this->search ) { | |
| 555 | - $value['s'] = $this->search; | |
| 556 | - // Every product/variation lane uses the same literal splitter. | |
| 557 | - $value['wcpos_search_phrase'] = $this->search; | |
| 558 | - } | |
| 559 | - } | |
| 560 | - if ( self::HOOK_POSTS_WHERE === $hook && ! empty( $this->rules['visibility']['where_backstop'] ) ) { | |
| 561 | - $value = ( new Pos_Visibility() )->apply_to_sql_where( $value, "{$wpdb->posts}.ID", $this->visibility_type ); | |
| 562 | - } | |
| 563 | - if ( '' === $this->search ) { | |
| 564 | - return $value; | |
| 565 | - } | |
| 566 | - if ( 'orders' === $this->collection ) { | |
| 567 | - if ( self::HOOK_HPOS_FILTERS === $hook ) { | |
| 568 | - $value['where'] .= ' AND ' . Order_Search::hpos_where( | |
| 569 | - $this->search, | |
| 570 | - array( | |
| 571 | - 'orders' => $query->get_table_name( 'orders' ), | |
| 572 | - 'addresses' => $query->get_table_name( 'addresses' ), | |
| 573 | - ), | |
| 574 | - $rule | |
| 575 | - ); | |
| 576 | - } elseif ( self::HOOK_POSTS_WHERE === $hook ) { | |
| 577 | - $value .= ' AND ' . Order_Search::posts_where( $this->search, $rule ); | |
| 578 | - } | |
| 579 | - } elseif ( 'variations' === $this->collection && 'wp_terms' === $rule['query'] ) { | |
| 580 | - switch ( $hook ) { | |
| 581 | - case 'posts_search': | |
| 582 | - return Product_Search::variation_posts_search( $value, $q, $rule ); | |
| 583 | - case 'posts_join': | |
| 584 | - return empty( $q['s'] ) ? $value : Product_Search::posts_join( $value, $q ); | |
| 585 | - case 'posts_groupby': | |
| 586 | - return empty( $q['s'] ) ? $value : Product_Search::posts_groupby( $value, $q ); | |
| 587 | - } | |
| 588 | - } elseif ( 'variations' === $this->collection ) { | |
| 589 | - if ( 'posts_groupby' === $hook ) { | |
| 590 | - $value = Product_Search::variation_groupby( $value, $q ); | |
| 591 | - } | |
| 592 | - } else { | |
| 593 | - switch ( $hook ) { | |
| 594 | - case 'posts_search': | |
| 595 | - return Product_Search::posts_search( $value, $q, $rule ); | |
| 596 | - case 'posts_join': | |
| 597 | - return Product_Search::posts_join( $value, $q ); | |
| 598 | - case 'posts_groupby': | |
| 599 | - return Product_Search::posts_groupby( $value, $q ); | |
| 600 | - case 'search_orderby': | |
| 601 | - if ( $rule['rank_exact'] ) { | |
| 602 | - $value['orderby'] = Product_Search::posts_orderby( (string) ( $value['orderby'] ?? '' ), $q, $rule ); | |
| 603 | - } | |
| 604 | - } | |
| 605 | - } | |
| 606 | - return $value; | |
| 607 | 445 | } |
| 608 | 446 | |
| 609 | 447 | /** |
| 610 | 448 | * Claim the `orderby` param when its value names a sort this collection declares. |