← All changes
|
includes/API/V1/Product_Variations_Controller.php
+89
-98
1.10.2
→
1.10.18
View file →
| @@ -17,9 +17,12 @@ | ||
| 17 | 17 | use WC_Data; |
| 18 | 18 | use WC_REST_Product_Variations_Controller; |
| 19 | 19 | use WCPOS\WooCommercePOS\Logger; |
| 20 | 20 | use WCPOS\WooCommercePOS\Services\Barcode_Field; |
| 21 | +use WCPOS\WooCommercePOS\Sync\Collection_Rules; | |
| 22 | +use WCPOS\WooCommercePOS\Sync\Collection_Rules_Plan; | |
| 21 | 23 | use WCPOS\WooCommercePOS\Sync\Pos_Visibility; |
| 24 | +use WCPOS\WooCommercePOS\Sync\Product_Search; | |
| 22 | 25 | use WP_Error; |
| 23 | 26 | use WP_Query; |
| 24 | 27 | use WP_REST_Request; |
| 25 | 28 | use WP_REST_Response; |
| @@ -45,13 +48,28 @@ | ||
| 45 | 48 | |
| 46 | 49 | /** |
| 47 | 50 | * Store the request object for use in lifecycle methods. |
| 48 | 51 | * |
| 49 | - * @var WP_REST_Request | |
| 52 | + * Null until `wcpos_dispatch_request()` runs: the instance exists, and its filters are | |
| 53 | + * registered, before any request is assigned — which is why the readers guard with | |
| 54 | + * `isset()`. Matches the same property on `API\V1\Products_Controller`. | |
| 55 | + * | |
| 56 | + * @var null|WP_REST_Request | |
| 50 | 57 | */ |
| 51 | 58 | protected $wcpos_request; |
| 52 | 59 | |
| 53 | 60 | /** |
| 61 | + * Request keys the variation Collection Rules plan reads on this lane. | |
| 62 | + * | |
| 63 | + * @var array | |
| 64 | + */ | |
| 65 | + private const WCPOS_SORT_PARAM_MAP = array( | |
| 66 | + 'orderby' => 'orderby', | |
| 67 | + 'order' => 'order', | |
| 68 | + 'search' => 'search', | |
| 69 | + ); | |
| 70 | + | |
| 71 | + /** | |
| 54 | 72 | * Dispatch request to parent controller, or override if needed. |
| 55 | 73 | * |
| 56 | 74 | * @param mixed $dispatch_result Dispatch result, will be used if not empty. |
| 57 | 75 | * @param WP_REST_Request $request Request used to generate the response. |
| @@ -63,9 +81,8 @@ | ||
| 63 | 81 | |
| 64 | 82 | add_filter( 'woocommerce_rest_prepare_product_variation_object', array( $this, 'wcpos_variation_response' ), 10, 3 ); |
| 65 | 83 | add_action( 'woocommerce_rest_insert_product_variation_object', array( $this, 'wcpos_insert_product_variation_object' ), 10, 3 ); |
| 66 | 84 | add_filter( 'woocommerce_rest_product_variation_object_query', array( $this, 'wcpos_product_variation_query' ), 10, 2 ); |
| 67 | - add_filter( 'posts_search', array( $this, 'wcpos_posts_search' ), 10, 2 ); | |
| 68 | 85 | |
| 69 | 86 | /* |
| 70 | 87 | * Check if the request is for all products and if the 'posts_per_page' is set to -1. |
| 71 | 88 | * Optimised query for getting all product IDs. |
| @@ -77,8 +94,23 @@ | ||
| 77 | 94 | return $dispatch_result; |
| 78 | 95 | } |
| 79 | 96 | |
| 80 | 97 | /** |
| 98 | + * Apply the collection's declared rules for nested and flat direct reads. | |
| 99 | + * | |
| 100 | + * @param WP_REST_Request $request Full details about the request. | |
| 101 | + * @return \WP_Error|\WP_REST_Response | |
| 102 | + */ | |
| 103 | + public function get_items( $request ) { | |
| 104 | + $plan = Collection_Rules::for_request( 'variations', $request, self::WCPOS_SORT_PARAM_MAP ); | |
| 105 | + return $plan->around( | |
| 106 | + function () use ( $request ) { | |
| 107 | + return parent::get_items( $request ); | |
| 108 | + } | |
| 109 | + ); | |
| 110 | + } | |
| 111 | + | |
| 112 | + /** | |
| 81 | 113 | * Register routes. |
| 82 | 114 | */ |
| 83 | 115 | public function register_routes(): void { |
| 84 | 116 | parent::register_routes(); |
| @@ -168,18 +200,19 @@ | ||
| 168 | 200 | if ( isset( $params['per_page'] ) && \is_array( $params['per_page'] ) ) { |
| 169 | 201 | $params['per_page']['minimum'] = -1; |
| 170 | 202 | } |
| 171 | 203 | |
| 204 | + // Search text is literal on every lane: `sanitize_text_field` would strip `%30` | |
| 205 | + // and blank malformed UTF-8 before the declared search rule ever saw them. | |
| 206 | + if ( isset( $params['search'] ) && \is_array( $params['search'] ) ) { | |
| 207 | + $params['search']['sanitize_callback'] = 'rest_sanitize_request_arg'; | |
| 208 | + } | |
| 209 | + | |
| 172 | 210 | // Ensure 'orderby' is set and is an array before attempting to modify it. |
| 173 | 211 | if ( isset( $params['orderby']['enum'] ) && \is_array( $params['orderby']['enum'] ) ) { |
| 174 | - // Define new sorting options. | |
| 175 | - $new_sort_options = array( | |
| 176 | - 'sku', | |
| 177 | - 'barcode', | |
| 178 | - 'stock_quantity', | |
| 179 | - 'stock_status', | |
| 180 | - ); | |
| 181 | - // Merge new options, avoiding duplicates. | |
| 212 | + // DECLARED once, in Sync\Collection_Rules, and projected here — so a sort cannot | |
| 213 | + // be advertised on one lane and rejected on the other. | |
| 214 | + $new_sort_options = Collection_Rules::orderby_enum( 'variations' ); | |
| 182 | 215 | $params['orderby']['enum'] = array_unique( array_merge( $params['orderby']['enum'], $new_sort_options ) ); |
| 183 | 216 | } |
| 184 | 217 | |
| 185 | 218 | return $params; |
| @@ -258,45 +291,12 @@ | ||
| 258 | 291 | * @param string $search Search string. |
| 259 | 292 | * @param WP_Query $wp_query WP_Query object. |
| 260 | 293 | * |
| 261 | 294 | * @return string |
| 295 | + * @deprecated Collection Rules now installs this behavior. | |
| 262 | 296 | */ |
| 263 | 297 | public function wcpos_posts_search( string $search, WP_Query $wp_query ) { |
| 264 | - global $wpdb; | |
| 265 | - | |
| 266 | - if ( empty( $search ) ) { | |
| 267 | - return $search; // skip processing - no search term in query. | |
| 268 | - } | |
| 269 | - | |
| 270 | - $q = $wp_query->query_vars; | |
| 271 | - $n = ! empty( $q['exact'] ) ? '' : '%'; | |
| 272 | - $search_terms = (array) $q['search_terms']; | |
| 273 | - | |
| 274 | - // Fields in the main 'posts' table. | |
| 275 | - $post_fields = array(); // nothing at the moment for variations. | |
| 276 | - | |
| 277 | - // Meta fields to search. | |
| 278 | - $meta_fields = Barcode_Field::search_keys(); | |
| 279 | - | |
| 280 | - $search_conditions = array(); | |
| 281 | - | |
| 282 | - foreach ( $search_terms as $term ) { | |
| 283 | - $term = $n . $wpdb->esc_like( $term ) . $n; | |
| 284 | - | |
| 285 | - // Search in meta fields. | |
| 286 | - foreach ( $meta_fields as $field ) { | |
| 287 | - $search_conditions[] = $wpdb->prepare( '(pm1.meta_value LIKE %s AND pm1.meta_key = %s)', $term, $field ); | |
| 288 | - } | |
| 289 | - } | |
| 290 | - | |
| 291 | - if ( ! empty( $search_conditions ) ) { | |
| 292 | - $search = ' AND (' . implode( ' OR ', $search_conditions ) . ') '; | |
| 293 | - if ( ! is_user_logged_in() ) { | |
| 294 | - $search .= " AND ($wpdb->posts.post_password = '') "; | |
| 295 | - } | |
| 296 | - } | |
| 297 | - | |
| 298 | - return $search; | |
| 298 | + return Product_Search::variation_posts_search( $search, $wp_query->query_vars, Collection_Rules::rules( 'variations' )['search'] ); | |
| 299 | 299 | } |
| 300 | 300 | |
| 301 | 301 | /** |
| 302 | 302 | * Filters the JOIN clause of the query. |
| @@ -304,17 +304,12 @@ | ||
| 304 | 304 | * @param string $join The JOIN clause of the query. |
| 305 | 305 | * @param WP_Query $query The WP_Query instance (passed by reference). |
| 306 | 306 | * |
| 307 | 307 | * @return string |
| 308 | + * @deprecated Collection Rules now installs this behavior. | |
| 308 | 309 | */ |
| 309 | 310 | public function wcpos_posts_join_to_posts_search( string $join, WP_Query $query ) { |
| 310 | - global $wpdb; | |
| 311 | - | |
| 312 | - if ( ! empty( $query->query_vars['s'] ) && false === strpos( $join, 'pm1' ) ) { | |
| 313 | - $join .= " LEFT JOIN {$wpdb->postmeta} pm1 ON {$wpdb->posts}.ID = pm1.post_id "; | |
| 314 | - } | |
| 315 | - | |
| 316 | - return $join; | |
| 311 | + return empty( $query->query_vars['s'] ) ? $join : Product_Search::posts_join( $join, $query->query_vars ); | |
| 317 | 312 | } |
| 318 | 313 | |
| 319 | 314 | /** |
| 320 | 315 | * Filters the GROUP BY clause of the query. |
| @@ -322,17 +317,12 @@ | ||
| 322 | 317 | * @param string $groupby The GROUP BY clause of the query. |
| 323 | 318 | * @param WP_Query $query The WP_Query instance (passed by reference). |
| 324 | 319 | * |
| 325 | 320 | * @return string |
| 321 | + * @deprecated Collection Rules now installs this behavior. | |
| 326 | 322 | */ |
| 327 | 323 | public function wcpos_posts_groupby_posts_search( string $groupby, WP_Query $query ) { |
| 328 | - global $wpdb; | |
| 329 | - | |
| 330 | - if ( ! empty( $query->query_vars['s'] ) ) { | |
| 331 | - $groupby = "{$wpdb->posts}.ID"; | |
| 332 | - } | |
| 333 | - | |
| 334 | - return $groupby; | |
| 324 | + return empty( $query->query_vars['s'] ) ? $groupby : Product_Search::posts_groupby( $groupby, $query->query_vars ); | |
| 335 | 325 | } |
| 336 | 326 | |
| 337 | 327 | /** |
| 338 | 328 | * Filter the query arguments for a request. |
| @@ -342,21 +332,10 @@ | ||
| 342 | 332 | * |
| 343 | 333 | * @return array $args Key value array of query var to query value. |
| 344 | 334 | */ |
| 345 | 335 | public function wcpos_product_variation_query( array $args, WP_REST_Request $request ) { |
| 346 | - if ( ! empty( $request['search'] ) ) { | |
| 347 | - // We need to set the query up for a postmeta join. | |
| 348 | - add_filter( 'posts_join', array( $this, 'wcpos_posts_join_to_posts_search' ), 10, 2 ); | |
| 349 | - add_filter( 'posts_groupby', array( $this, 'wcpos_posts_groupby_posts_search' ), 10, 2 ); | |
| 350 | - } | |
| 351 | - | |
| 352 | - // if POS only products are enabled, exclude online-only products. | |
| 353 | - if ( $this->wcpos_pos_only_products_enabled() ) { | |
| 354 | - add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_variation_exclude_online_only' ), 10, 2 ); | |
| 355 | - } | |
| 356 | - | |
| 357 | 336 | // Check for wcpos_include/wcpos_exclude parameter. |
| 358 | - // NOTE: do this after POS visibility filter so that takes precedence. | |
| 337 | + // The Collection Rules visibility backstop runs first, at priority 10. | |
| 359 | 338 | if ( isset( $request['wcpos_include'] ) || isset( $request['wcpos_exclude'] ) ) { |
| 360 | 339 | add_filter( 'posts_where', array( $this, 'wcpos_posts_where_product_variation_include_exclude' ), 20, 2 ); |
| 361 | 340 | } |
| 362 | 341 | |
| @@ -372,8 +351,9 @@ | ||
| 372 | 351 | * @param string $where The WHERE clause of the query. |
| 373 | 352 | * @param WP_Query $query The WP_Query instance (passed by reference). |
| 374 | 353 | * |
| 375 | 354 | * @return string |
| 355 | + * @deprecated Collection Rules now installs this behavior. | |
| 376 | 356 | */ |
| 377 | 357 | public function wcpos_posts_where_product_variation_exclude_online_only( string $where, WP_Query $query ) { |
| 378 | 358 | global $wpdb; |
| 379 | 359 | |
| @@ -455,13 +435,41 @@ | ||
| 455 | 435 | * |
| 456 | 436 | * @param WP_REST_Request $request Full details about the request. |
| 457 | 437 | */ |
| 458 | 438 | public function wcpos_get_all_items( $request ) { |
| 459 | - return parent::get_items( $request ); | |
| 439 | + return $this->get_items( $request ); | |
| 460 | 440 | } |
| 461 | 441 | |
| 462 | 442 | |
| 463 | 443 | /** |
| 444 | + * Apply the declared POS variation sorts to the SQL clauses. | |
| 445 | + * | |
| 446 | + * `posts_clauses` fires for EVERY WP_Query, so the body is guarded by post type and by | |
| 447 | + * the plan itself — it contributes nothing unless this request claimed one of the | |
| 448 | + * declared sorts. | |
| 449 | + * | |
| 450 | + * @param array $clauses Associative array of the clauses for the query. | |
| 451 | + * @param WP_Query $wp_query The WP_Query instance. | |
| 452 | + * | |
| 453 | + * @return array | |
| 454 | + * @deprecated Collection Rules now installs this behavior. | |
| 455 | + */ | |
| 456 | + public function wcpos_posts_clauses( array $clauses, WP_Query $wp_query ): array { | |
| 457 | + if ( ! isset( $this->wcpos_request ) ) { | |
| 458 | + return $clauses; | |
| 459 | + } | |
| 460 | + | |
| 461 | + $post_type = $wp_query->query_vars['post_type'] ?? null; | |
| 462 | + if ( 'product_variation' !== $post_type && ( ! \is_array( $post_type ) || ! \in_array( 'product_variation', $post_type, true ) ) ) { | |
| 463 | + return $clauses; | |
| 464 | + } | |
| 465 | + | |
| 466 | + $plan = Collection_Rules::for_request( 'variations', $this->wcpos_request, self::WCPOS_SORT_PARAM_MAP ); | |
| 467 | + | |
| 468 | + return $plan->filter( Collection_Rules_Plan::HOOK_POSTS_CLAUSES, $clauses, $wp_query ); | |
| 469 | + } | |
| 470 | + | |
| 471 | + /** | |
| 464 | 472 | * Prepare objects query. |
| 465 | 473 | * |
| 466 | 474 | * @param WP_REST_Request $request Full details about the request. |
| 467 | 475 | * |
| @@ -469,33 +477,16 @@ | ||
| 469 | 477 | */ |
| 470 | 478 | protected function prepare_objects_query( $request ) { |
| 471 | 479 | $args = parent::prepare_objects_query( $request ); |
| 472 | 480 | |
| 473 | - // Add custom 'orderby' options. | |
| 474 | - if ( isset( $request['orderby'] ) ) { | |
| 475 | - switch ( $request['orderby'] ) { | |
| 476 | - case 'sku': | |
| 477 | - $args['meta_key'] = '_sku'; | |
| 478 | - $args['orderby'] = 'meta_value'; | |
| 481 | + /* | |
| 482 | + * The POS sorts (`sku`, `barcode`, `stock_quantity`, `stock_status`) are NOT mapped | |
| 483 | + * onto `meta_key` + `orderby => meta_value` here any more. That pair INNER JOINs | |
| 484 | + * postmeta, so it dropped every variation with no value for the key — a sort acting | |
| 485 | + * as a filter. `Sync\Collection_Rules` declares them and its scoped plan | |
| 486 | + * applies them as a LEFT JOIN, on this lane and on `wcpos/v2` alike. | |
| 487 | + */ | |
| 479 | 488 | |
| 480 | - break; | |
| 481 | - case 'barcode': | |
| 482 | - $args['meta_key'] = Barcode_Field::orderby_key(); | |
| 483 | - $args['orderby'] = 'meta_value'; | |
| 484 | - | |
| 485 | - break; | |
| 486 | - case 'stock_quantity': | |
| 487 | - $args['meta_key'] = '_stock'; | |
| 488 | - $args['orderby'] = 'meta_value_num'; | |
| 489 | - | |
| 490 | - break; | |
| 491 | - case 'stock_status': | |
| 492 | - $args['meta_key'] = '_stock_status'; | |
| 493 | - $args['orderby'] = 'meta_value'; | |
| 494 | - | |
| 495 | - break; | |
| 496 | - } | |
| 497 | - } | |
| 498 | - | |
| 499 | - return $args; | |
| 489 | + $plan = Collection_Rules::for_request( 'variations', $request, self::WCPOS_SORT_PARAM_MAP ); | |
| 490 | + return $plan->filter( Collection_Rules_Plan::HOOK_PREPARE_ARGS, $args ); | |
| 500 | 491 | } |
| 501 | 492 | } |