| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Legacy WCPOS search API; retained for Pro for one release. | |
| 3 | + * Shared product search SQL filters. | |
| 4 | 4 | * |
| 5 | 5 | * @package WCPOS\WooCommercePOS\API |
| 6 | 6 | */ |
| 7 | 7 | |
| @@ -6,63 +6,120 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | namespace WCPOS\WooCommercePOS\API; |
| 9 | 9 | |
| 10 | -use WCPOS\WooCommercePOS\Sync\Collection_Rules; | |
| 11 | -use WCPOS\WooCommercePOS\Sync\Product_Search as Search_Rule; | |
| 10 | +use WCPOS\WooCommercePOS\Services\Barcode_Field; | |
| 12 | 11 | use WP_Query; |
| 13 | 12 | |
| 14 | 13 | /** |
| 15 | - * Legacy search entry points. | |
| 16 | - * | |
| 17 | - * @deprecated Search is owned by Sync\Collection_Rules. | |
| 14 | + * Keeps v1 and v2 product search fields identical. | |
| 18 | 15 | */ |
| 19 | 16 | final class Product_Search { |
| 17 | + private const MAX_SEARCH_TERMS = 10; | |
| 18 | + | |
| 20 | 19 | /** |
| 21 | - * Build product search SQL. | |
| 20 | + * Search product titles, SKUs, and the configured barcode field. | |
| 22 | 21 | * |
| 23 | - * @deprecated Use the Collection Rules plan. | |
| 24 | - * | |
| 25 | 22 | * @param string $search Search SQL. |
| 26 | 23 | * @param WP_Query $wp_query Query instance. |
| 27 | 24 | * @return string |
| 28 | 25 | */ |
| 29 | 26 | public static function posts_search( string $search, WP_Query $wp_query ): string { |
| 30 | - return Search_Rule::posts_search( $search, $wp_query->query_vars, Collection_Rules::rules( 'products' )['search'] ); | |
| 27 | + global $wpdb; | |
| 28 | + $q = $wp_query->query_vars; | |
| 29 | + $phrase = $q['wcpos_search_phrase'] ?? null; | |
| 30 | + if ( empty( $search ) && null === $phrase ) { | |
| 31 | + return $search; | |
| 32 | + } | |
| 33 | + $terms = null !== $phrase | |
| 34 | + ? preg_split( '/[\s\p{Z}\p{C}]+/u', $phrase, -1, PREG_SPLIT_NO_EMPTY ) | |
| 35 | + : (array) $q['search_terms']; | |
| 36 | + if ( null !== $phrase && ( false === $terms || array() === $terms ) ) { | |
| 37 | + return ' AND 1=0 '; | |
| 38 | + } | |
| 39 | + // Like WP_Query::parse_search() and WooCommerce's search_products(), collapse over-long lists to the phrase. | |
| 40 | + if ( null !== $phrase && self::MAX_SEARCH_TERMS < \count( $terms ) ) { | |
| 41 | + $terms = array( $phrase ); | |
| 42 | + } | |
| 43 | + $n = ! empty( $q['exact'] ) ? '' : '%'; | |
| 44 | + $meta_fields = Barcode_Field::search_keys(); | |
| 45 | + $meta_placeholders = implode( ', ', array_fill( 0, \count( $meta_fields ), '%s' ) ); | |
| 46 | + $search_conditions = array(); | |
| 47 | + foreach ( $terms as $term ) { | |
| 48 | + $term = $n . $wpdb->esc_like( $term ) . $n; | |
| 49 | + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names come from $wpdb; $meta_placeholders is a generated list of %s placeholders, and the keys themselves are passed to prepare() as arguments. | |
| 50 | + $search_conditions[] = $wpdb->prepare( | |
| 51 | + "( {$wpdb->posts}.post_title LIKE %s OR EXISTS ( | |
| 52 | + SELECT 1 FROM {$wpdb->postmeta} AS wcpos_search_meta WHERE wcpos_search_meta.post_id = {$wpdb->posts}.ID AND wcpos_search_meta.meta_key IN ($meta_placeholders) AND wcpos_search_meta.meta_value LIKE %s | |
| 53 | + ) )", | |
| 54 | + array_merge( array( $term ), $meta_fields, array( $term ) ) | |
| 55 | + ); | |
| 56 | + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 57 | + } | |
| 58 | + | |
| 59 | + if ( ! empty( $search_conditions ) ) { | |
| 60 | + $search = ' AND (' . implode( ' AND ', $search_conditions ) . ') '; | |
| 61 | + if ( ! is_user_logged_in() ) { | |
| 62 | + $search .= " AND ($wpdb->posts.post_password = '') "; | |
| 63 | + } | |
| 64 | + } | |
| 65 | + return $search; | |
| 31 | 66 | } |
| 67 | + | |
| 32 | 68 | /** |
| 33 | - * Join product search metadata. | |
| 69 | + * Join product meta while searching. | |
| 34 | 70 | * |
| 35 | - * @deprecated Use the Collection Rules plan. | |
| 36 | - * | |
| 37 | 71 | * @param string $join JOIN SQL. |
| 38 | 72 | * @param WP_Query $query Query instance. |
| 39 | 73 | * @return string |
| 40 | 74 | */ |
| 41 | 75 | public static function posts_join( string $join, WP_Query $query ): string { |
| 42 | - return Search_Rule::posts_join( $join, $query->query_vars ); | |
| 76 | + global $wpdb; | |
| 77 | + if ( self::is_searching( $query ) && false === strpos( $join, 'pm1' ) ) { | |
| 78 | + $join .= " LEFT JOIN {$wpdb->postmeta} pm1 ON {$wpdb->posts}.ID = pm1.post_id "; | |
| 79 | + } | |
| 80 | + return $join; | |
| 43 | 81 | } |
| 82 | + | |
| 44 | 83 | /** |
| 45 | - * Group product search results. | |
| 84 | + * Group product search results after joining meta. | |
| 46 | 85 | * |
| 47 | - * @deprecated Use the Collection Rules plan. | |
| 48 | - * | |
| 49 | 86 | * @param string $groupby GROUP BY SQL. |
| 50 | 87 | * @param WP_Query $query Query instance. |
| 51 | 88 | * @return string |
| 52 | 89 | */ |
| 53 | 90 | public static function posts_groupby( string $groupby, WP_Query $query ): string { |
| 54 | - return Search_Rule::posts_groupby( $groupby, $query->query_vars ); | |
| 91 | + global $wpdb; | |
| 92 | + if ( self::is_searching( $query ) ) { | |
| 93 | + $groupby = "{$wpdb->posts}.ID"; | |
| 94 | + } | |
| 95 | + return $groupby; | |
| 55 | 96 | } |
| 97 | + | |
| 56 | 98 | /** |
| 57 | - * Rank exact product matches. | |
| 99 | + * Rank exact SKU or barcode matches ahead of substring matches. | |
| 58 | 100 | * |
| 59 | - * @deprecated Use the Collection Rules plan. | |
| 60 | - * | |
| 61 | 101 | * @param string $orderby ORDER BY SQL. |
| 62 | 102 | * @param WP_Query $query Query instance. |
| 63 | - * @return string | |
| 64 | 103 | */ |
| 65 | 104 | public static function posts_orderby( string $orderby, WP_Query $query ): string { |
| 66 | - return Search_Rule::posts_orderby( $orderby, $query->query_vars, Collection_Rules::rules( 'products' )['search'] ); | |
| 105 | + global $wpdb; | |
| 106 | + if ( ! self::is_searching( $query ) ) { | |
| 107 | + return $orderby; | |
| 108 | + } | |
| 109 | + $keys = Barcode_Field::search_keys(); | |
| 110 | + return $wpdb->prepare( | |
| 111 | + 'MIN(CASE WHEN pm1.meta_key IN (' . implode( ', ', array_fill( 0, count( $keys ), '%s' ) ) . ') AND pm1.meta_value = %s THEN 0 ELSE 1 END) ASC', | |
| 112 | + array_merge( $keys, array( trim( (string) ( $query->query_vars['wcpos_search_phrase'] ?? $query->query_vars['s'] ) ) ) ) | |
| 113 | + ) . ', ' . ( '' === trim( $orderby ) ? "{$wpdb->posts}.ID DESC" : $orderby ); | |
| 114 | + } | |
| 115 | + | |
| 116 | + /** | |
| 117 | + * Whether the query carries a search term. Not empty(): the literal term "0" is a search. | |
| 118 | + * | |
| 119 | + * @param WP_Query $query Query instance. | |
| 120 | + * @return bool | |
| 121 | + */ | |
| 122 | + private static function is_searching( WP_Query $query ): bool { | |
| 123 | + return '' !== (string) ( $query->query_vars['wcpos_search_phrase'] ?? $query->query_vars['s'] ?? '' ); | |
| 67 | 124 | } |
| 68 | 125 | } |