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.19 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 All 163 releases
← All changes | includes/API/V2/Variations_Controller.php +169 -127 1.10.11.10.18 View file →
@@ -8,13 +8,14 @@
8 8 namespace WCPOS\WooCommercePOS\API\V2;
9 9
10 10 use WC_Product_Variation;
11 11 use WC_REST_Product_Variations_Controller;
12 -use WCPOS\WooCommercePOS\Services\Barcode_Field;
13 12 use WCPOS\WooCommercePOS\Sync\Api;
13 +use WCPOS\WooCommercePOS\Sync\Collection_Rules;
14 +use WCPOS\WooCommercePOS\Sync\Collection_Rules_Plan;
14 15 use WCPOS\WooCommercePOS\Sync\Digest_Index;
15 16 use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions;
16 -use WCPOS\WooCommercePOS\Sync\Pos_Visibility;
17 +use WCPOS\WooCommercePOS\Sync\Product_Search;
17 18 use WCPOS\WooCommercePOS\Sync\Product_Serializer;
18 19 use WP_Error;
19 20 use WP_Query;
20 21 use WP_REST_Request;
@@ -23,13 +24,15 @@
23 24
24 25 // phpcs:disable Squiz.Commenting, Generic.Commenting -- Ported lab documentation is preserved verbatim.
25 26
26 27 /**
27 - * Variations document endpoint (on-demand variation fetch).
28 + * Variations document endpoint — the collection's hydration AND list/seed lane (ADR 0034).
28 29 *
29 30 * Why a flat route: the change-signal yields BARE variation ids (no parent), and WooCommerce's
30 31 * only variation routes are parent-mediated (`products/<parent>/variations`). One flat route
31 - * lets the client pull a deferred variation set in ONE round trip with no parent->child dance.
32 + * IS the cross-parent collection: bare pages seed the complete replica (the idle trickle),
33 + * `include=` is one filter on it (targeted hydration, no parent->child dance), and the
34 + * SKU/barcode discovery search is another.
32 35 *
33 36 * Why it EXTENDS WooCommerce's variations controller: because that is all the route ever needed.
34 37 * WooCommerce's `get_objects()` already answers a cross-parent query — with no `product_id` in
35 38 * the route there is no parent constraint, and `include`/`search`/`orderby`/pagination are its
@@ -47,14 +50,31 @@
47 50 * (`documents[].{id,parent_id,payload,_rxdb_digest}`), POS visibility, the barcode carrier
48 51 * search, and the request bounds. Everything else is WooCommerce's.
49 52 */
50 53 class Variations_Controller extends WC_REST_Product_Variations_Controller {
54 + /**
55 + * Request keys the variation Collection Rules plan reads on this lane.
56 + *
57 + * @var array
58 + */
59 + private const WCPOS_SORT_PARAM_MAP = array(
60 + 'orderby' => 'orderby',
61 + 'order' => 'order',
62 + 'search' => 'search',
63 + );
64 +
65 + /**
66 + * The request whose declared rules wrap the collection query.
67 + *
68 + * @var null|WP_REST_Request
69 + */
70 + private $wcpos_sort_request = null;
71 +
51 72 use Endpoint_Permissions;
52 73
53 74 private const MAX_SKU_LENGTH = 4096;
54 75 private const MAX_SKU_TERMS = 100;
55 76 private const MAX_SEARCH_LENGTH = 256;
56 - private const MAX_SEARCH_TERMS = 10;
57 77 private const MAX_PAGE = 1000;
58 78
59 79
60 80 public function register_routes(): void {
@@ -86,9 +106,13 @@
86 106 /**
87 107 * Narrow WooCommerce's variation query to what the POS may serve.
88 108 *
89 109 * Everything WooCommerce already understands — `include`, `offset`, `order`, pagination,
90 - * status — comes from `parent::prepare_objects_query()`. Layered on top: POS visibility, the
110 + * status — comes from `parent::prepare_objects_query()`, which also applies
111 + * `woocommerce_rest_product_variation_object_query` internally (wc/v3's CRUD controller fires
112 + * it there, not in `get_items()`), so third-party query scoping reaches every lane built
113 + * through this method. Layered on top — deliberately AFTER that filter, so a third party
114 + * cannot widen what the POS may serve: POS visibility, the
91 115 * barcode-carrier search, and the sort keys the POS grids offer. This is the seam 1.9.x used
92 116 * for the same job (`API\V1\Product_Variations_Controller::prepare_objects_query`).
93 117 *
94 118 * @param WP_REST_Request $request Full details about the request.
@@ -127,44 +151,8 @@
127 151 */
128 152 $args['post_type'] = $this->post_type;
129 153
130 154 /*
131 - * `search` means the barcode CARRIERS here, not the post title.
132 - *
133 - * WooCommerce maps `search` onto `s`, which searches post_title/content — useless for a
134 - * variation, whose title is a generated attribute string. The POS searches what a cashier
135 - * actually types or scans: the SKU and whichever meta key the store configured as its
136 - * barcode field (`Barcode_Field::search_keys()`). Any term matching any carrier wins,
137 - * which is the semantics the previous hand-rolled SQL had and the specs pin.
138 - *
139 - * `sku` is left to WooCommerce: its own exact/comma-list handling is what the
140 - * sku-beats-search precedence rule relies on.
141 - */
142 - $search = (string) ( $request->get_param( 'search' ) ?? '' );
143 - if ( '' !== $sku ) {
144 - // SKU is an exact lookup and outranks a fuzzy one; leaving WooCommerce's post-title
145 - // `s` in place would AND the two and return nothing.
146 - unset( $args['s'] );
147 - }
148 - if ( '' !== $search && '' === $sku ) {
149 - unset( $args['s'] );
150 - $args['wcpos_variation_search'] = true;
151 - $carriers = array( 'relation' => 'OR' );
152 - foreach ( (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY ) as $term ) {
153 - foreach ( Barcode_Field::search_keys() as $key ) {
154 - $carriers[] = array(
155 - 'key' => $key,
156 - 'value' => $term,
157 - 'compare' => 'LIKE',
158 - );
159 - }
160 - }
161 - if ( 1 < \count( $carriers ) ) {
162 - $args['meta_query'] = $this->add_meta_query( $args, $carriers ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
163 - }
164 - }
165 -
166 - /*
167 155 * This route only ever offers what the store owner has for sale — on EVERY lane, including
168 156 * `include`.
169 157 *
170 158 * WooCommerce's Enabled checkbox on the variation metabox writes `post_status = private`
@@ -183,61 +171,36 @@
183 171 * Set after `parent::prepare_objects_query()` so an explicit `status` param cannot widen it.
184 172 */
185 173 $args['post_status'] = 'publish';
186 174
187 - /*
188 - * Leg-3 (ADR 0014 WP-M5): POS-hidden (`online_only`) variations are never served. As a
189 - * query exclusion rather than a post-hoc filter of the result, so paging and totals count
190 - * the same set the client is allowed to see.
191 - *
192 - * Through the helper, NOT a raw `post__not_in` merge: `parent::prepare_objects_query()`
193 - * maps `include` to `post__in`, and WP_Query IGNORES `post__not_in` when `post__in` is
194 - * present — so `?search=X&include=<hidden id>` would have served a hidden variation.
195 - * `apply_to_wp_query_args()` already owns that trap: it intersects `post__in` with the
196 - * hidden set and pins an empty intersection to `array( 0 )`.
197 - */
198 - $args = ( new Pos_Visibility() )->apply_to_wp_query_args( $args, Pos_Visibility::VARIATIONS );
175 + $this->wcpos_sort_request = $request;
176 + $plan = Collection_Rules::for_request( 'variations', $request, self::WCPOS_SORT_PARAM_MAP );
177 + $args = $plan->filter( Collection_Rules_Plan::HOOK_PREPARE_ARGS, $args );
199 178
200 - /*
201 - * The POS sorts on fields WooCommerce does not offer as orderby values. They are declared
202 - * in get_collection_params() below — without that, `orderby=sku` is rejected by REST
203 - * argument validation before this switch ever runs.
204 - */
205 - if ( isset( $request['orderby'] ) ) {
206 - switch ( $request['orderby'] ) {
207 - case 'sku':
208 - $args['meta_key'] = '_sku'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
209 - $args['orderby'] = 'meta_value';
179 + return $args;
180 + }
210 181
211 - break;
212 - case 'barcode':
213 - $args['meta_key'] = Barcode_Field::orderby_key(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
214 - $args['orderby'] = 'meta_value';
215 -
216 - break;
217 - case 'stock_quantity':
218 - $args['meta_key'] = '_stock'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
219 - $args['orderby'] = 'meta_value_num';
220 -
221 - break;
222 - case 'stock_status':
223 - $args['meta_key'] = '_stock_status'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
224 - $args['orderby'] = 'meta_value';
225 -
226 - break;
182 + /** Apply the same rule topology to discovery, collection pages, and named includes. */
183 + protected function get_objects( $query_args ) {
184 + $plan = Collection_Rules::for_request( 'variations', $this->wcpos_sort_request, self::WCPOS_SORT_PARAM_MAP );
185 + return $plan->around(
186 + function () use ( $query_args ) {
187 + return parent::get_objects( $query_args );
227 188 }
228 - }
229 -
230 - return $args;
189 + );
231 190 }
232 191
233 192 /**
234 - * GET /variations?include=12,34,56 — hydrate the given variation ids.
193 + * GET /variations — the flat collection's three lanes, one response shape.
235 194 *
236 - * Mirrors the wc/v3 `products?include=` shape; the parent is resolved
237 - * server-side off the loaded variation object (get_parent_id), so the client
238 - * never needs to know parents. Unknown / non-variation ids are skipped
239 - * (deletes are handled by the change-signal tombstone path, not here).
195 + * `?sku=`/`?search=` discovers by barcode carrier; a bare request serves one
196 + * collection page (the trickle's seed lane); `?include=12,34` hydrates the
197 + * named ids. All three resolve ids through WooCommerce's collection query,
198 + * then hydrate through the shared assembly line below. Mirrors the wc/v3
199 + * `products?include=` shape; the parent is resolved server-side off the
200 + * loaded variation object (get_parent_id), so the client never needs to know
201 + * parents. Unknown / non-variation ids are skipped (deletes are handled by
202 + * the change-signal tombstone path, not here).
240 203 */
241 204 public function get_variations( WP_REST_Request $request ) {
242 205 $started = microtime( true );
243 206 $search_meta = null;
@@ -259,13 +222,58 @@
259 222 * filter, and a collection route with no filter is a collection.
260 223 */
261 224 list( $ids, $search_meta ) = $this->collection_page( $request );
262 225 } else {
263 - $ids = array_values( array_unique( array_map( 'intval', (array) $request->get_param( 'include' ) ) ) );
264 - // Leg-3 (ADR 0014 WP-M5): drop POS-hidden (`online_only`) variations from the served set. A hidden
265 - // id simply isn't hydrated → the client's targeted pull returns nothing for it → Leg-3 prunes it.
266 - // (Products get the equivalent exclusion via the catalog-proxy `post__not_in` filter.)
267 - $ids = ( new Pos_Visibility() )->filter_visible_children( $ids );
226 + /*
227 + * The ask runs through the SAME query WooCommerce's own collection read builds
228 + * (#1751): `parent::prepare_objects_query()` maps `include` to `post__in` and — in
229 + * wc/v3's CRUD controller — applies `woocommerce_rest_product_variation_object_query`
230 + * internally, so third-party query scoping reaches this lane like every other
231 + * (hook-parity contract #1738). The collection and discovery lanes always had that
232 + * property; this lane loaded ids directly and bypassed it. POS visibility and the
233 + * publish gate ride the same args (layered in our override).
234 + *
235 + * The paging/ordering params are PINNED, not honoured: this lane answers a named
236 + * ask, so `per_page` covers the whole ask, `offset`/`page` cannot skip any of it
237 + * (a skipped id is absent from documents, which the client reads as "prune this
238 + * id"), and `orderby=include` keeps WooCommerce from ordering by a meta key whose
239 + * EXISTS join would silently drop every variation lacking that meta row. Pinning
240 + * `orderby` also keeps the args complete for direct (non-dispatched) invocations,
241 + * which carry no route defaults. Served order is the include order either way —
242 + * the intersect below is the final authority.
243 + */
244 + $include_ids = array_values( array_unique( array_map( 'intval', (array) $request->get_param( 'include' ) ) ) );
245 + // Pins live on a QUERY-ONLY clone: the dispatched request stays exactly
246 + // as the client sent it, for the serializer's prepare-filters and for
247 + // anything downstream reading it after dispatch.
248 + $query_request = clone $request;
249 + $query_request->set_param( 'per_page', max( 1, count( $include_ids ) ) );
250 + $query_request->set_param( 'page', 1 );
251 + $query_request->set_param( 'offset', 0 );
252 + $query_request->set_param( 'orderby', 'include' );
253 + $query_request->set_param( 'order', 'asc' );
254 + $args = $this->prepare_objects_query( $query_request );
255 +
256 + /*
257 + * The ask is a CEILING. WooCommerce's variations controller UNIONS some
258 + * collection params into `post__in` (`on_sale=true` array-unions every on-sale
259 + * id on top of the ask), so without this intersection a stray param would
260 + * hydrate the whole store into the till. No request param or filter may widen
261 + * the served set beyond the named ids — narrowing is fine, that is what the
262 + * object_query filter and the visibility exclusion are for. An emptied ask pins
263 + * to `array( 0 )`, the same never-matches sentinel Pos_Visibility uses.
264 + */
265 + $post_in = array_values( array_intersect( array_map( 'intval', (array) ( $args['post__in'] ?? array() ) ), $include_ids ) );
266 + $args['post__in'] = array() === $post_in ? array( 0 ) : $post_in;
267 + $results = $this->get_objects( $args );
268 +
269 + $allowed_ids = array();
270 + foreach ( $results['objects'] as $object ) {
271 + if ( $object instanceof WC_Product_Variation ) {
272 + $allowed_ids[] = $object->get_id();
273 + }
274 + }
275 + $ids = array_values( array_intersect( $include_ids, $allowed_ids ) );
268 276 }
269 277 _prime_post_caches( $ids, true, true );
270 278
271 279 // Hydrate through THE product assembly line (Product_Serializer), the same
@@ -284,10 +292,15 @@
284 292 $digests = class_exists( Digest_Index::class )
285 293 ? ( new Digest_Index() )->read_digests( 'products', $ids )
286 294 : array();
287 295
288 - $serialization_request = new WP_REST_Request( 'GET', '/' );
289 - $serializer = new Product_Serializer();
296 + $serializer = new Product_Serializer();
297 + // A CLONE of the live request, not a synthetic bare one (so prepare-filters
298 + // see the real request context), and not the live request itself (the
299 + // serializer stamps store scope and a per-variation `product_id` onto
300 + // whatever it is handed; the dispatched request must leave this method as
301 + // the client sent it).
302 + $serialization_request = clone $request;
290 303 $documents = array();
291 304 foreach ( $ids as $id ) {
292 305 $variation = wc_get_product( $id );
293 306 if ( ! $variation instanceof WC_Product_Variation ) {
@@ -294,15 +307,14 @@
294 307 continue;
295 308 }
296 309 /*
297 310 * DISABLED variations are never hydrated — see the `post_status` note in
298 - * prepare_objects_query(). The gate lives here as well because the `include` lane does
299 - * not build a WP_Query at all: it loads each id directly, so the query-level narrowing
300 - * that covers the collection and discovery lanes cannot reach it.
311 + * prepare_objects_query(). The query-level publish gate covers ALL lanes, including
312 + * `include`; this check only guards a status change between the id query and object load.
301 313 *
302 - * Dropping it here (rather than out of $ids) deliberately leaves `meta.requested`
303 - * counting the ask: requested > returned is precisely the shortfall the client's
304 - * targeted pull reads as "prune this id".
314 + * `meta.requested` now counts the query-eligible ask: a disabled or query-filtered id is
315 + * absent from $ids. The client's targeted-pull shortfall — absence from documents — is
316 + * unchanged.
305 317 */
306 318 if ( 'publish' !== $variation->get_status() ) {
307 319 continue;
308 320 }
@@ -375,13 +387,18 @@
375 387 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'sku must not contain more than 100 comma-separated terms', array( 'status' => 400 ) );
376 388 }
377 389 } else {
378 390 $search = (string) $request->get_param( 'search' );
379 - if ( self::MAX_SEARCH_LENGTH < \strlen( $search ) ) {
380 - return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not exceed 256 bytes', array( 'status' => 400 ) );
391 + // Unlike mb_strlen(), PCRE is independent of blog_charset and detects malformed UTF-8.
392 + $characters = preg_match_all( '/./us', $search );
393 + if ( false === $characters ) {
394 + return new WP_Error( 'woocommerce_pos_variations_search_invalid', 'search must be valid UTF-8', array( 'status' => 400 ) );
381 395 }
382 - $terms = (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
383 - if ( self::MAX_SEARCH_TERMS < \count( $terms ) ) {
396 + if ( self::MAX_SEARCH_LENGTH < $characters ) {
397 + return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not exceed 256 characters', array( 'status' => 400 ) );
398 + }
399 + $terms = Collection_Rules::search_terms( trim( $search ) );
400 + if ( Collection_Rules::rules( 'variations' )['search']['term_cap'] < \count( $terms ) ) {
384 401 return new WP_Error( 'woocommerce_pos_variations_search_limit_exceeded', 'search must not contain more than 10 whitespace-separated terms', array( 'status' => 400 ) );
385 402 }
386 403 }
387 404
@@ -392,8 +409,36 @@
392 409 return true;
393 410 }
394 411
395 412 /**
413 + * Apply the declared POS variation sorts to the SQL clauses.
414 + *
415 + * `posts_clauses` fires for EVERY WP_Query, so the body is guarded by post type and by
416 + * the plan itself — it contributes nothing unless this request claimed one of the
417 + * declared sorts.
418 + *
419 + * @param array $clauses Associative array of the clauses for the query.
420 + * @param WP_Query $wp_query The WP_Query instance.
421 + *
422 + * @deprecated Collection Rules now installs this behavior; retained for Pro callers.
423 + * @return array
424 + */
425 + public function wcpos_posts_clauses( array $clauses, WP_Query $wp_query ): array {
426 + if ( null === $this->wcpos_sort_request ) {
427 + return $clauses;
428 + }
429 +
430 + $post_type = $wp_query->query_vars['post_type'] ?? null;
431 + if ( 'product_variation' !== $post_type && ( ! \is_array( $post_type ) || ! \in_array( 'product_variation', $post_type, true ) ) ) {
432 + return $clauses;
433 + }
434 +
435 + $plan = Collection_Rules::for_request( 'variations', $this->wcpos_sort_request, self::WCPOS_SORT_PARAM_MAP );
436 +
437 + return $plan->filter( Collection_Rules_Plan::HOOK_POSTS_CLAUSES, $clauses, $wp_query );
438 + }
439 +
440 + /**
396 441 * WooCommerce's collection params, plus the sort keys the POS grids offer.
397 442 *
398 443 * `orderby` is a validated enum. Appending here is what lets `prepare_objects_query()` act on
399 444 * these four — otherwise the request 400s during argument validation and the switch is dead
@@ -400,8 +445,9 @@
400 445 * code. 1.9.x extended the same enum for the same reason.
401 446 */
402 447 public function get_collection_params() {
403 448 $params = parent::get_collection_params();
449 + $params['search']['sanitize_callback'] = 'rest_sanitize_request_arg';
404 450
405 451 if ( isset( $params['orderby']['enum'] ) && \is_array( $params['orderby']['enum'] ) ) {
406 452 $params['orderby']['enum'] = array_values(
407 453 array_unique(
@@ -406,9 +452,9 @@
406 452 $params['orderby']['enum'] = array_values(
407 453 array_unique(
408 454 array_merge(
409 455 $params['orderby']['enum'],
410 - array( 'sku', 'barcode', 'stock_quantity', 'stock_status' )
456 + Collection_Rules::orderby_enum( 'variations' )
411 457 )
412 458 )
413 459 );
414 460 }
@@ -420,13 +466,13 @@
420 466 * De-duplicate variation searches joined through matching meta rows.
421 467 *
422 468 * @param string $groupby Existing GROUP BY clause.
423 469 * @param WP_Query $query Query being filtered.
470 + *
471 + * @deprecated Collection Rules owns variation grouping.
424 472 */
425 473 public function group_search_results( string $groupby, WP_Query $query ): string {
426 - global $wpdb;
427 -
428 - return ! empty( $query->query_vars['wcpos_variation_search'] ) ? "{$wpdb->posts}.ID" : $groupby;
474 + return Product_Search::variation_groupby( $groupby, $query->query_vars );
429 475 }
430 476
431 477 /**
432 478 * Does this discovery request still carry a term after normalization?
@@ -440,24 +486,14 @@
440 486 return true;
441 487 }
442 488
443 489 $search = (string) ( $request->get_param( 'search' ) ?? '' );
490 + $terms = Collection_Rules::search_terms( trim( $search ) );
444 491
445 - return array() !== (array) preg_split( '/\s+/', trim( $search ), -1, PREG_SPLIT_NO_EMPTY );
492 + return array() !== $terms;
446 493 }
447 494
448 495 /**
449 - * Discover a page of published, POS-visible variation ids by SKU/barcode.
450 - *
451 - * The query is WooCommerce's — `prepare_objects_query()` + `get_objects()`, the same pair its
452 - * own `get_items()` uses. This method previously hand-built the SQL: a `wp_posts`/`wp_postmeta`
453 - * INNER JOIN with `LIKE` predicates assembled per (field, term) pair, a second COUNT(DISTINCT)
454 - * query for the total, and the hidden-id exclusion spliced into the same placeholder list. All
455 - * of it duplicated `WP_Query` — which is where such copies go wrong, quietly and later.
456 - *
457 - * @return array{0: array<int, int>, 1: array{total: int, page: int, per_page: int}}
458 - */
459 - /**
460 496 * One page of the POS-servable variation collection, with its total.
461 497 *
462 498 * WooCommerce's query pair, same as {@see search_variation_ids()} — the only difference is that
463 499 * a bare collection request carries no discovery constraint to normalize away, so the
@@ -491,8 +527,19 @@
491 527 ),
492 528 );
493 529 }
494 530
531 + /**
532 + * Discover a page of published, POS-visible variation ids by SKU/barcode.
533 + *
534 + * The query is WooCommerce's — `prepare_objects_query()` + `get_objects()`, the same pair its
535 + * own `get_items()` uses. This method previously hand-built the SQL: a `wp_posts`/`wp_postmeta`
536 + * INNER JOIN with `LIKE` predicates assembled per (field, term) pair, a second COUNT(DISTINCT)
537 + * query for the total, and the hidden-id exclusion spliced into the same placeholder list. All
538 + * of it duplicated `WP_Query` — which is where such copies go wrong, quietly and later.
539 + *
540 + * @return array{0: array<int, int>, 1: array{total: int, page: int, per_page: int}}
541 + */
495 542 private function search_variation_ids( WP_REST_Request $request ): array {
496 543 $per_page = max( 1, min( 100, (int) ( $request->get_param( 'per_page' ) ?? 10 ) ) );
497 544 $page = max( 1, (int) ( $request->get_param( 'page' ) ?? 1 ) );
498 545 $request->set_param( 'per_page', $per_page );
@@ -516,14 +563,9 @@
516 563 ),
517 564 );
518 565 }
519 566
520 - add_filter( 'posts_groupby', array( $this, 'group_search_results' ), 10, 2 );
521 - try {
522 - $results = $this->get_objects( $query_args );
523 - } finally {
524 - remove_filter( 'posts_groupby', array( $this, 'group_search_results' ), 10 );
525 - }
567 + $results = $this->get_objects( $query_args );
526 568
527 569 $ids = array();
528 570 foreach ( $results['objects'] as $object ) {
529 571 if ( $object instanceof WC_Product_Variation ) {