| 1 |
<?php |
| 2 |
/** |
| 3 |
* Stable secondary sort keys for the catalog proxy. |
| 4 |
* |
| 5 |
* @package WCPOS\WooCommercePOS\API\V2\Proxy |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace WCPOS\WooCommercePOS\API\V2\Proxy; |
| 9 |
|
| 10 |
/** |
| 11 |
* Pins a deterministic id tiebreak under mutable, tie-prone sort keys. |
| 12 |
* |
| 13 |
* ORDER BY a tied column alone gives MySQL no total order ACROSS separate |
| 14 |
* offset queries, so a row tied at a page boundary can appear on two pages or |
| 15 |
* on neither while the POS client walks a multi-page window (mono#1372). The |
| 16 |
* client renders ties in ascending id order (its declared tiebreak), so the |
| 17 |
* secondary key here is always `id ASC` — wire pages and rendered rows agree |
| 18 |
* exactly, whatever the primary direction. |
| 19 |
*/ |
| 20 |
final class Stable_Sort { |
| 21 |
/** |
| 22 |
* Post orderbys whose values routinely tie (duplicate titles, shared |
| 23 |
* import timestamps). `id`/`include` are total orders already. |
| 24 |
*/ |
| 25 |
private const TIED_POST_ORDERBYS = array( 'title', 'date', 'modified' ); |
| 26 |
|
| 27 |
/** |
| 28 |
* Rewrite a tied post-query orderby to its array form carrying the id tiebreak. |
| 29 |
* |
| 30 |
* @param array $args WP_Query args prepared by the wc/v3 controller. |
| 31 |
* |
| 32 |
* @return array |
| 33 |
*/ |
| 34 |
public static function with_post_id_tiebreak( array $args ): array { |
| 35 |
$orderby = $args['orderby'] ?? null; |
| 36 |
// WC's CRUD controller already appends ' ID' for date/modified ("for |
| 37 |
// consistency with pagination") — but in the space-string form, where the |
| 38 |
// GLOBAL direction applies to every field, so a desc sort ties by ID DESC |
| 39 |
// while the client renders ties by id ASC. Normalize that form back to the |
| 40 |
// bare key so the rewrite below pins ID ASC either way. |
| 41 |
if ( \is_string( $orderby ) && ' ID' === substr( $orderby, -3 ) ) { |
| 42 |
$orderby = substr( $orderby, 0, -3 ); |
| 43 |
} |
| 44 |
if ( ! \is_string( $orderby ) || ! \in_array( $orderby, self::TIED_POST_ORDERBYS, true ) ) { |
| 45 |
return $args; |
| 46 |
} |
| 47 |
$order = 'ASC' === strtoupper( (string) ( $args['order'] ?? 'DESC' ) ) ? 'ASC' : 'DESC'; |
| 48 |
$args['orderby'] = array( |
| 49 |
$orderby => $order, |
| 50 |
'ID' => 'ASC', |
| 51 |
); |
| 52 |
|
| 53 |
return $args; |
| 54 |
} |
| 55 |
|
| 56 |
/** |
| 57 |
* Append the term_id tiebreak to a tie-prone term-query ORDER BY clause. |
| 58 |
* |
| 59 |
* The get_terms API has no array-orderby form, so the tiebreak lands at the clause |
| 60 |
* level. Only `name` can tie (same-named children under different parents); |
| 61 |
* the direction moves INTO the orderby clause because WP appends the free |
| 62 |
* `order` to the LAST expression only. |
| 63 |
* |
| 64 |
* @param array $clauses WP_Term_Query SQL clauses. |
| 65 |
* |
| 66 |
* @return array |
| 67 |
*/ |
| 68 |
public static function with_term_id_tiebreak( array $clauses ): array { |
| 69 |
$orderby = (string) ( $clauses['orderby'] ?? '' ); |
| 70 |
if ( 'ORDER BY t.name' !== $orderby ) { |
| 71 |
return $clauses; |
| 72 |
} |
| 73 |
$order = 'DESC' === strtoupper( (string) ( $clauses['order'] ?? 'ASC' ) ) ? 'DESC' : 'ASC'; |
| 74 |
$clauses['orderby'] = $orderby . ' ' . $order . ', t.term_id'; |
| 75 |
$clauses['order'] = 'ASC'; |
| 76 |
|
| 77 |
return $clauses; |
| 78 |
} |
| 79 |
} |
| 80 |
|