6, 'category' => 3, 'course' => 4, 'lesson' => 4, 'post' => 6, 'page' => 2 ];
private const LIMITS_ALL = [ 'product' => 4, 'category' => 2, 'course' => 3, 'lesson' => 3, 'post' => 3, 'page' => 2 ];
private const TYPES = [
'learning' => [ 'rooster_course' => 'course', 'rooster_lesson' => 'lesson' ],
'blog' => [ 'post' => 'post' ],
'all' => [ 'rooster_course' => 'course', 'rooster_lesson' => 'lesson', 'post' => 'post', 'page' => 'page' ],
];
public static function boot(): void {
add_action( 'rest_api_init', [ self::class, 'routes' ] );
}
public static function routes(): void {
register_rest_route( self::NS, self::ROUTE, [
'methods' => 'GET',
'callback' => [ self::class, 'answer' ],
'permission_callback' => '__return_true',
'args' => [
'q' => [ 'type' => 'string', 'required' => true, 'minLength' => 2, 'maxLength' => 80 ],
'scope' => [ 'type' => 'string', 'enum' => [ 'product', 'learning', 'blog', 'all' ], 'default' => 'product' ],
],
] );
}
public static function answer( \WP_REST_Request $request ): \WP_REST_Response {
$q = trim( preg_replace( '/\s+/u', ' ', wp_strip_all_tags( (string) $request['q'] ) ) ?? '' );
$scope = (string) $request['scope'];
if ( 'product' === $scope && ! self::has_products() ) {
$scope = 'all';
}
$lang = (string) ( apply_filters( 'wpml_current_language', null ) ?? ( function_exists( 'pll_current_language' ) ? pll_current_language() : '' ) );
$shared = ! is_user_logged_in();
$key = md5( $q . '|' . $scope . '|' . $lang );
$items = $shared ? wp_cache_get( $key, 'rooster_suggest' ) : false;
if ( ! is_array( $items ) ) {
$items = mb_strlen( $q ) < 2 ? [] : self::items( $q, $scope );
if ( $shared ) {
wp_cache_set( $key, $items, 'rooster_suggest', 5 * MINUTE_IN_SECONDS );
}
}
$types = match ( $scope ) {
'product' => 'product',
'learning' => array_values( array_filter( array_keys( self::TYPES['learning'] ), 'post_type_exists' ) ),
'blog' => 'post',
default => '',
};
$all = add_query_arg( array_filter( [ 's' => rawurlencode( $q ), 'post_type' => $types ] ), home_url( '/' ) );
$response = new \WP_REST_Response( [ 'q' => $q, 'scope' => $scope, 'items' => $items, 'all' => $all ] );
$response->header( 'Cache-Control', $shared ? 'public, max-age=300' : 'private, no-store' );
return $response;
}
public static function items( string $q, string $scope ): array {
$items = [];
$limits = 'all' === $scope ? self::LIMITS_ALL : self::LIMITS;
if ( in_array( $scope, [ 'product', 'all' ], true ) && self::has_products() ) {
foreach ( self::posts( 'product', $q, $limits['product'], self::product_query_args() ) as $id ) {
$product = wc_get_product( $id );
if ( ! $product ) {
continue;
}
$items[] = self::item( 'product', $id ) + self::price( (string) $product->get_price_html() );
}
$terms = get_terms( [ 'taxonomy' => 'product_cat', 'name__like' => $q, 'number' => $limits['category'], 'hide_empty' => true, 'orderby' => 'count', 'order' => 'DESC' ] );
foreach ( is_array( $terms ) ? $terms : [] as $term ) {
$link = get_term_link( $term );
if ( is_string( $link ) ) {
$item = [ 'type' => 'category', 'title' => self::text( $term->name ), 'url' => $link, 'count' => (int) $term->count ];
$thumb = (int) get_term_meta( $term->term_id, 'thumbnail_id', true );
$image = $thumb ? wp_get_attachment_image_src( $thumb, 'woocommerce_gallery_thumbnail' ) : false;
$items[] = $image ? $item + [ 'image' => (string) $image[0] ] : $item;
}
}
}
foreach ( self::TYPES[ $scope ] ?? [] as $post_type => $type ) {
if ( ! post_type_exists( $post_type ) ) {
continue;
}
foreach ( self::posts( $post_type, $q, $limits[ $type ], 'page' === $type ? self::shop_pages() : [] ) as $id ) {
$items[] = self::item( $type, $id );
}
}
return array_values( array_filter( (array) apply_filters( 'rooster/suggest/items', $items, $q, $scope ), static fn ( $item ) => is_array( $item ) && ! empty( $item['title'] ) && ! empty( $item['url'] ) ) );
}
private static function shop_pages(): array {
$ids = function_exists( 'wc_get_page_id' ) ? array_filter( array_map( 'wc_get_page_id', [ 'cart', 'checkout', 'myaccount' ] ), static fn ( $id ) => $id > 0 ) : [];
return $ids ? [ 'post__not_in' => array_values( $ids ) ] : [];
}
private static function has_products(): bool {
return function_exists( 'wc_get_product' ) && post_type_exists( 'product' );
}
private static function product_query_args(): array {
$hidden = function_exists( 'wc_get_product_visibility_term_ids' ) ? wc_get_product_visibility_term_ids() : [];
$ids = array_filter( [ $hidden['exclude-from-search'] ?? 0, 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ? ( $hidden['outofstock'] ?? 0 ) : 0 ] );
return $ids ? [ 'tax_query' => [ [ 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array_values( $ids ), 'operator' => 'NOT IN' ] ] ] : [];
}
private static function posts( string $post_type, string $q, int $limit, array $extra = [] ): array {
$query = new \WP_Query( $extra + [
'post_type' => $post_type,
'post_status' => 'publish',
's' => $q,
'posts_per_page' => $limit,
'fields' => 'ids',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_term_cache' => false,
'suppress_filters' => false,
] );
return array_map( 'intval', $query->posts );
}
private static function item( string $type, int $id ): array {
$item = [ 'type' => $type, 'title' => self::text( get_the_title( $id ) ), 'url' => (string) get_permalink( $id ) ];
$thumb = get_post_thumbnail_id( $id );
$image = $thumb ? wp_get_attachment_image_src( (int) $thumb, 'product' === $type && has_image_size( 'woocommerce_gallery_thumbnail' ) ? 'woocommerce_gallery_thumbnail' : 'thumbnail' ) : false;
if ( $image ) {
$item['image'] = (string) $image[0];
}
return $item;
}
public static function price( string $html ): array {
$html = (string) preg_replace( '#.*?#s', '', $html );
$out = [];
if ( preg_match( '#]*>(.*?)#s', $html, $m ) ) {
$out['was'] = self::text( $m[1] );
$html = str_replace( $m[0], '', $html );
}
return [ 'price' => self::text( $html ) ] + $out;
}
private static function text( string $html ): string {
$text = html_entity_decode( wp_strip_all_tags( $html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
return trim( (string) preg_replace( '/\s+/u', ' ', $text ) );
}
}