[ 'search' => callable( $term, $limit ), * 'labels' => callable( int[] $ids ) ]. Both return a list of [ id, label, meta ]. * * @since 1.0.0 */ final class Lookup { /** * Max results per search. */ const LIMIT = 20; /** * Registered sources. * * @since 1.0.0 * @return array */ public static function sources() { /** * Filter the lookup sources available to `search` settings fields. * * @since 1.0.0 * @param array $sources Key => [ search => callable, labels => callable, string? => bool, * capability? => string (extra capability `GET apwc/v1/lookup` needs) ]. */ return (array) apply_filters( 'apwc_lookup_sources', array( 'products' => array( 'search' => array( __CLASS__, 'search_products' ), 'labels' => array( __CLASS__, 'label_products' ), ), 'product_cat' => array( 'search' => static fn( $term, $limit ) => self::search_terms( 'product_cat', $term, $limit ), 'labels' => static fn( $ids ) => self::label_terms( 'product_cat', $ids ), ), 'product_tag' => array( 'search' => static fn( $term, $limit ) => self::search_terms( 'product_tag', $term, $limit ), 'labels' => static fn( $ids ) => self::label_terms( 'product_tag', $ids ), ), 'shipping_classes' => array( 'search' => static fn( $term, $limit ) => self::search_terms( 'product_shipping_class', $term, $limit ), 'labels' => static fn( $ids ) => self::label_terms( 'product_shipping_class', $ids ), ), 'users' => array( 'search' => array( __CLASS__, 'search_users' ), 'labels' => array( __CLASS__, 'label_users' ), ), // Text keys: fixed lists searched in memory. 'countries' => self::fixed( array( __CLASS__, 'countries' ) ), 'states' => self::fixed( array( __CLASS__, 'states' ) ), 'roles' => self::fixed( array( __CLASS__, 'roles' ) ), 'gateways' => self::fixed( array( __CLASS__, 'gateways' ) ), 'shipping_methods' => self::fixed( array( __CLASS__, 'shipping_methods' ) ), 'product_types' => self::fixed( array( __CLASS__, 'product_types' ) ), ) ); } /** * Search a source. * * @since 1.0.0 * @param string $source Source key. * @param string $term Search text ('' = first results). * @return array */ public static function search( $source, $term ) { $sources = self::sources(); if ( ! isset( $sources[ $source ]['search'] ) || ! is_callable( $sources[ $source ]['search'] ) ) { return array(); } return array_values( (array) call_user_func( $sources[ $source ]['search'], (string) $term, self::LIMIT ) ); } /** * Labels for saved IDs, in the given order; unknown IDs are dropped. * * @since 1.0.0 * @param string $source Source key. * @param array $ids IDs. * @return array */ public static function labels( $source, array $ids ) { $ids = self::clean_ids( $source, $ids ); $sources = self::sources(); if ( ! $ids || ! isset( $sources[ $source ]['labels'] ) || ! is_callable( $sources[ $source ]['labels'] ) ) { return array(); } $found = array(); foreach ( (array) call_user_func( $sources[ $source ]['labels'], $ids ) as $item ) { $found[ (string) $item['id'] ] = $item; } $ordered = array(); foreach ( $ids as $id ) { if ( isset( $found[ (string) $id ] ) ) { $ordered[] = $found[ (string) $id ]; } } return $ordered; } /** * IDs that exist in the source. * * @since 1.0.0 * @param string $source Source key. * @param array $ids IDs. * @return array */ public static function existing( $source, array $ids ) { $string = ! empty( self::sources()[ $source ]['string'] ); return array_map( static fn( $item ) => $string ? (string) $item['id'] : (int) $item['id'], self::labels( $source, $ids ) ); } /** * Clean IDs for a source: numbers, or text keys for fixed lists. * * @since 1.0.0 * @param string $source Source key. * @param array $ids Raw IDs. * @return array */ public static function clean_ids( $source, array $ids ) { if ( ! empty( self::sources()[ $source ]['string'] ) ) { return array_values( array_unique( array_filter( array_map( static fn( $id ) => sanitize_text_field( (string) $id ), $ids ), static fn( $id ) => '' !== $id ) ) ); } return array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) ); } /** * A source over a fixed key => label list (searched in memory, text keys). * * @since 1.0.0 * @param callable $options Returns key => label. * @return array{string: true, search: callable, labels: callable} */ public static function fixed( callable $options ) { return array( 'string' => true, 'search' => static function ( $term, $limit ) use ( $options ) { $term = strtolower( trim( (string) $term ) ); $ranks = array( array(), array(), array() ); // Exact, starts with, contains. foreach ( (array) call_user_func( $options ) as $key => $label ) { $name = strtolower( (string) $label ); $code = strtolower( (string) $key ); if ( '' === $term || $name === $term || $code === $term ) { $rank = 0; } elseif ( 0 === strpos( $name, $term ) ) { $rank = 1; } elseif ( false !== strpos( $name . ' ' . $code, $term ) ) { $rank = 2; } else { continue; } $ranks[ $rank ][] = array( 'id' => (string) $key, 'label' => (string) $label, 'meta' => (string) $key, ); } return array_slice( array_merge( ...$ranks ), 0, $limit ); }, 'labels' => static function ( $ids ) use ( $options ) { $all = (array) call_user_func( $options ); $out = array(); foreach ( $ids as $id ) { if ( isset( $all[ $id ] ) ) { $out[] = array( 'id' => (string) $id, 'label' => (string) $all[ $id ], 'meta' => (string) $id, ); } } return $out; }, ); } /** * Countries. * * @since 1.0.0 * @return array */ public static function countries() { return function_exists( 'WC' ) ? (array) WC()->countries->get_countries() : array(); } /** * States as "CC:ST" => "State, Country". * * @since 1.0.0 * @return array */ public static function states() { if ( ! function_exists( 'WC' ) ) { return array(); } $countries = WC()->countries->get_countries(); $out = array(); foreach ( (array) WC()->countries->get_states() as $cc => $states ) { foreach ( (array) $states as $code => $name ) { $out[ $cc . ':' . $code ] = html_entity_decode( $name ) . ', ' . ( $countries[ $cc ] ?? $cc ); } } return $out; } /** * Roles, plus "guest" for shoppers without an account on this site (logged out, or no role here). * * @since 1.0.0 * @return array */ public static function roles() { $roles = array( 'guest' => __( 'Guest (no account on this site)', 'admin-powerups-for-woocommerce' ) ); foreach ( wp_roles()->get_names() as $key => $name ) { $roles[ $key ] = translate_user_role( $name ); } return $roles; } /** * Payment gateways. * * @since 1.0.0 * @return array */ public static function gateways() { $out = array(); if ( function_exists( 'WC' ) ) { foreach ( WC()->payment_gateways()->payment_gateways() as $id => $gateway ) { $title = wp_strip_all_tags( (string) ( $gateway->get_method_title() ? $gateway->get_method_title() : $gateway->get_title() ) ); $out[ $id ] = 'yes' === $gateway->enabled ? $title : sprintf( /* translators: %s: payment method. */ __( '%s (off)', 'admin-powerups-for-woocommerce' ), $title ); } } return $out; } /** * Shipping methods: each type ("any Flat rate") and each method in each zone ("flat_rate:3"). * * @since 1.0.0 * @return array */ public static function shipping_methods() { if ( ! function_exists( 'WC' ) || ! class_exists( '\WC_Shipping_Zones' ) ) { return array(); } $out = array(); foreach ( WC()->shipping()->get_shipping_methods() as $id => $method ) { /* translators: %s: shipping method type, e.g. "Flat rate". */ $out[ $id ] = sprintf( __( 'Any %s', 'admin-powerups-for-woocommerce' ), $method->get_method_title() ); } $zones = \WC_Shipping_Zones::get_zones(); $zones[] = array( 'zone_name' => __( 'Rest of the world', 'admin-powerups-for-woocommerce' ), 'shipping_methods' => ( new \WC_Shipping_Zone( 0 ) )->get_shipping_methods(), ); foreach ( $zones as $zone ) { foreach ( (array) $zone['shipping_methods'] as $method ) { $out[ $method->id . ':' . $method->instance_id ] = $method->get_title() . ' · ' . $zone['zone_name']; } } return $out; } /** * Product types and flags. * * @since 1.0.0 * @return array */ public static function product_types() { $types = function_exists( 'wc_get_product_types' ) ? (array) wc_get_product_types() : array(); return $types + array( 'virtual' => __( 'Virtual', 'admin-powerups-for-woocommerce' ), 'downloadable' => __( 'Downloadable', 'admin-powerups-for-woocommerce' ), ); } /** * Users by name, email or login. * * @since 1.0.0 * @param string $term Text. * @param int $limit Max. * @return array */ public static function search_users( $term, $limit ) { $args = array( 'number' => $limit, 'fields' => array( 'ID', 'display_name', 'user_email' ), 'orderby' => 'display_name', ); if ( '' !== trim( (string) $term ) ) { $args['search'] = '*' . trim( (string) $term ) . '*'; $args['search_columns'] = array( 'user_login', 'user_email', 'display_name' ); } return array_map( static fn( $u ) => array( 'id' => (int) $u->ID, 'label' => (string) $u->display_name, 'meta' => (string) $u->user_email, ), ( new \WP_User_Query( $args ) )->get_results() ); } /** * Users by ID. * * @since 1.0.0 * @param int[] $ids IDs. * @return array */ public static function label_users( array $ids ) { return array_map( static fn( $u ) => array( 'id' => (int) $u->ID, 'label' => (string) $u->display_name, 'meta' => (string) $u->user_email, ), get_users( array( 'include' => $ids, 'fields' => array( 'ID', 'display_name', 'user_email' ), ) ) ); } /** * Products and variations by name, SKU or ID. * * @since 1.0.0 * @param string $term Text. * @param int $limit Max. * @return array */ public static function search_products( $term, $limit ) { $term = trim( $term ); if ( '' === $term ) { $ids = wc_get_products( array( 'limit' => $limit, 'return' => 'ids', 'orderby' => 'title', 'order' => 'ASC', 'status' => array( 'publish', 'private', 'draft' ), ) ); } else { // WC_Data_Store forwards search_products() to the product data store through __call(). $ids = (array) call_user_func( array( WC_Data_Store::load( 'product' ), 'search_products' ), $term, '', true, true, $limit ); if ( ctype_digit( $term ) && wc_get_product( (int) $term ) ) { array_unshift( $ids, (int) $term ); } } return self::label_products( array_slice( array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) ), 0, $limit ) ); } /** * Product labels: "Name – attributes", meta "SKU · #ID". * * @since 1.0.0 * @param int[] $ids IDs. * @return array */ public static function label_products( array $ids ) { $items = array(); foreach ( $ids as $id ) { $product = wc_get_product( $id ); if ( ! $product ) { continue; } $meta = array_filter( array( $product->get_sku(), '#' . $product->get_id() ) ); $items[] = array( 'id' => $product->get_id(), 'label' => wp_strip_all_tags( $product->get_name() ), 'meta' => implode( ' · ', $meta ), ); } return $items; } /** * Terms by name, with their parent path. * * @since 1.0.0 * @param string $taxonomy Taxonomy. * @param string $term Text. * @param int $limit Max. * @return array */ public static function search_terms( $taxonomy, $term, $limit ) { $terms = get_terms( array( 'taxonomy' => $taxonomy, 'search' => trim( $term ), 'number' => $limit, 'hide_empty' => false, 'orderby' => 'name', ) ); return is_wp_error( $terms ) ? array() : array_map( array( __CLASS__, 'term_item' ), $terms ); } /** * Term labels. * * @since 1.0.0 * @param string $taxonomy Taxonomy. * @param int[] $ids IDs. * @return array */ public static function label_terms( $taxonomy, array $ids ) { $terms = get_terms( array( 'taxonomy' => $taxonomy, 'include' => $ids, 'hide_empty' => false, ) ); return is_wp_error( $terms ) ? array() : array_map( array( __CLASS__, 'term_item' ), $terms ); } /** * Item for a term: label "Parent › Child", meta "N products". * * @since 1.0.0 * @param \WP_Term $term Term. * @return array{id: int, label: string, meta: string} */ private static function term_item( $term ) { $path = array(); foreach ( array_reverse( get_ancestors( $term->term_id, $term->taxonomy, 'taxonomy' ) ) as $ancestor_id ) { $ancestor = get_term( $ancestor_id, $term->taxonomy ); if ( $ancestor && ! is_wp_error( $ancestor ) ) { $path[] = $ancestor->name; } } $path[] = $term->name; return array( 'id' => (int) $term->term_id, 'label' => implode( ' › ', $path ), /* translators: %d: number of products. */ 'meta' => sprintf( _n( '%d product', '%d products', (int) $term->count, 'admin-powerups-for-woocommerce' ), (int) $term->count ), ); } }