get_meta( self::COND_META, true ) ) ); if ( '' === $c && $p->get_parent_id() ) { $c = strtolower( trim( (string) get_post_meta( $p->get_parent_id(), self::COND_META, true ) ) ); } return in_array( $c, self::CONDITIONS, true ) ? $c : 'new'; } /** v1: price as the shop shows it (woocommerce_tax_display_shop), "89.00"; null when empty. */ public static function display_price( \WC_Product $p, $raw ): ?string { if ( '' === (string) $raw || ! is_numeric( $raw ) ) { return null; } return number_format( (float) wc_get_price_to_display( $p, array( 'price' => $raw ) ), 2, '.', '' ); } /** v1: Merchant API parity fields (same sources as the XML feed). */ public static function v1_fields( \WC_Product $p ): array { $parent = $p->get_parent_id() ? wc_get_product( $p->get_parent_id() ) : null; $parent = $parent instanceof \WC_Product ? $parent : null; $base = $parent ? $parent : $p; [ , $gallery ] = Feed::images( $p, $parent ); $from = $p->get_date_on_sale_from( 'edit' ); $to = $p->get_date_on_sale_to( 'edit' ); $w = (string) $p->get_weight(); $dims = array( 'length' => (string) $p->get_length(), 'width' => (string) $p->get_width(), 'height' => (string) $p->get_height(), ); $avail = Feed::availability( (string) $p->get_stock_status() ); $adate = ( 'backorder' === $avail || 'preorder' === $avail ) ? Feed::availability_date( $p, $parent ) : ''; $gcat = self::google_category( $base->get_id() ); $reg = $p->get_regular_price(); return array( 'parent_id' => $parent ? $parent->get_id() : null, 'gallery_image_urls' => array_values( $gallery ), 'google_product_category' => '' === $gcat ? null : $gcat, 'sale_price_from' => $from ? $from->date( 'c' ) : null, 'sale_price_to' => $to ? $to->date( 'c' ) : null, 'weight' => '' === $w ? null : $w, 'weight_unit' => '' === $w ? null : (string) get_option( 'woocommerce_weight_unit', 'kg' ), 'dimensions' => '' === implode( '', $dims ) ? null : array_merge( array_map( static fn( $v ) => '' === $v ? null : $v, $dims ), array( 'unit' => (string) get_option( 'woocommerce_dimension_unit', 'cm' ) ) ), 'availability_date' => '' === $adate ? null : $adate, 'display_regular_price' => self::display_price( $p, '' === (string) $reg ? $p->get_price() : $reg ), 'display_sale_price' => $p->is_on_sale( 'edit' ) || '' !== (string) $p->get_sale_price() ? self::display_price( $p, $p->get_sale_price() ) : null, 'prices_include_tax' => function_exists( 'wc_prices_include_tax' ) ? (bool) wc_prices_include_tax() : false, 'condition' => self::condition( $p ), ); } /** v0.3: the approved FAQ of a product, as a list of {q, a} (plain text). Empty when there is none. */ public static function faq( \WC_Product $p ): array { $raw = $p->get_meta( self::FAQ_META, true ); if ( ! is_array( $raw ) ) { return array(); } $out = array(); foreach ( $raw as $item ) { if ( is_array( $item ) && isset( $item['q'], $item['a'] ) && is_string( $item['q'] ) && is_string( $item['a'] ) && '' !== $item['q'] ) { $out[] = array( 'q' => $item['q'], 'a' => $item['a'], ); } } return $out; } public static function brand_uses_taxonomy(): bool { return taxonomy_exists( self::BRAND_TAX ); } /** Brand names for a product (first is the primary brand). */ public static function brand_names( int $product_id ): array { if ( self::brand_uses_taxonomy() ) { $terms = get_the_terms( $product_id, self::BRAND_TAX ); if ( is_array( $terms ) ) { return array_values( array_map( static fn( $t ) => html_entity_decode( $t->name, ENT_QUOTES, 'UTF-8' ), $terms ) ); } return array(); } $b = (string) get_post_meta( $product_id, self::BRAND_META, true ); return '' === $b ? array() : array( $b ); } public static function brand( \WC_Product $p ): string { $id = $p->get_parent_id() ? $p->get_parent_id() : $p->get_id(); $names = self::brand_names( $id ); return $names ? (string) $names[0] : ''; } /** * Brand for the feed and JSON-LD: the product's own brand, else the merchant's explicit store-wide * default_brand (own-brand stores only; empty unless the merchant set it). Never guessed. */ public static function effective_brand( \WC_Product $p ): string { $b = self::brand( $p ); if ( '' !== $b ) { return $b; } return (string) ( Settings::all()['default_brand'] ?? '' ); } /** Google product category for a product from the merchant's category map (deepest mapped category or ancestor), or ''. */ public static function google_category( int $product_id ): string { $map = Settings::all()['google_category_map']; if ( ! $map ) { return ''; } $terms = get_the_terms( $product_id, 'product_cat' ); if ( ! is_array( $terms ) || ! $terms ) { return ''; } $best = ''; $best_depth = -1; foreach ( $terms as $t ) { $chain = array( (int) $t->term_id ); $cur = $t; $guard = 0; while ( $cur->parent && $guard++ < 10 ) { $cur = get_term( $cur->parent, 'product_cat' ); if ( ! $cur || is_wp_error( $cur ) ) { break; } $chain[] = (int) $cur->term_id; } // $chain is deepest-first; the first mapped entry is the most specific mapping on this branch. foreach ( $chain as $i => $tid ) { if ( isset( $map[ $tid ] ) || isset( $map[ (string) $tid ] ) ) { $depth = count( $chain ) - $i; if ( $depth > $best_depth ) { $best_depth = $depth; $best = (string) ( $map[ $tid ] ?? $map[ (string) $tid ] ); } break; } } } return $best; } public static function gtin( \WC_Product $p ): string { if ( ! method_exists( $p, 'get_global_unique_id' ) ) { return ''; } $g = (string) $p->get_global_unique_id( 'edit' ); if ( '' === $g && $p->get_parent_id() ) { return ''; // Variation GTIN must be its own; never inherit the parent's. } return $g; } public static function mpn( \WC_Product $p ): string { $m = (string) $p->get_meta( self::MPN_META, true ); if ( '' === $m && $p->get_parent_id() ) { return ''; } return $m; } public static function image_url( \WC_Product $p ): string { $img = $p->get_image_id(); if ( ! $img && $p->get_parent_id() ) { $parent = wc_get_product( $p->get_parent_id() ); $img = $parent ? $parent->get_image_id() : 0; } $url = $img ? wp_get_attachment_image_url( (int) $img, 'full' ) : ''; return $url ? (string) $url : ''; } public static function category_names( int $product_id ): array { $terms = get_the_terms( $product_id, 'product_cat' ); if ( ! is_array( $terms ) ) { return array(); } return array_values( array_map( static fn( $t ) => html_entity_decode( $t->name, ENT_QUOTES, 'UTF-8' ), $terms ) ); } /** "Parent > Child" path of the deepest category, for g:product_type. */ public static function category_path( int $product_id ): string { $terms = get_the_terms( $product_id, 'product_cat' ); if ( ! is_array( $terms ) || ! $terms ) { return ''; } $best = array(); foreach ( $terms as $t ) { $chain = array( $t->name ); $cur = $t; $guard = 0; while ( $cur->parent && $guard++ < 10 ) { $cur = get_term( $cur->parent, 'product_cat' ); if ( ! $cur || is_wp_error( $cur ) ) { break; } array_unshift( $chain, $cur->name ); } if ( count( $chain ) > count( $best ) ) { $best = $chain; } } return html_entity_decode( implode( ' > ', $best ), ENT_QUOTES, 'UTF-8' ); } /** "Parent > Child" label for one product_cat term (admin display). */ public static function category_path_for_term( int $term_id ): string { $t = get_term( $term_id, 'product_cat' ); if ( ! $t || is_wp_error( $t ) ) { return ''; } $chain = array( $t->name ); $guard = 0; while ( $t->parent && $guard++ < 10 ) { $t = get_term( $t->parent, 'product_cat' ); if ( ! $t || is_wp_error( $t ) ) { break; } array_unshift( $chain, $t->name ); } return html_entity_decode( implode( ' > ', $chain ), ENT_QUOTES, 'UTF-8' ); } public static function attributes( \WC_Product $p ): array { $out = array(); if ( $p->is_type( 'variation' ) ) { foreach ( $p->get_attributes() as $tax => $val ) { $label = wc_attribute_label( str_replace( 'attribute_', '', $tax ), $p ); $name = $val; if ( taxonomy_exists( $tax ) ) { $term = get_term_by( 'slug', $val, $tax ); $name = $term ? $term->name : $val; } $out[ $label ] = array( (string) $name ); } return $out; } foreach ( $p->get_attributes() as $attr ) { if ( ! is_object( $attr ) || ! method_exists( $attr, 'get_name' ) ) { continue; } if ( method_exists( $attr, 'get_visible' ) && ! $attr->get_visible() && ! $attr->get_variation() ) { continue; } $label = wc_attribute_label( $attr->get_name(), $p ); if ( $attr->is_taxonomy() ) { $vals = array_map( static fn( $t ) => $t->name, (array) $attr->get_terms() ); } else { $vals = array_map( 'strval', (array) $attr->get_options() ); } $out[ html_entity_decode( $label, ENT_QUOTES, 'UTF-8' ) ] = array_values( array_map( static fn( $v ) => html_entity_decode( (string) $v, ENT_QUOTES, 'UTF-8' ), $vals ) ); } return $out; } public static function normalize( \WC_Product $p ): array { $modified = $p->get_date_modified(); return array( 'id' => $p->get_id(), 'url' => (string) get_permalink( $p->get_id() ), 'title' => html_entity_decode( (string) $p->get_name(), ENT_QUOTES, 'UTF-8' ), 'sku' => (string) $p->get_sku(), 'type' => (string) $p->get_type(), 'price' => (string) $p->get_price(), 'regular_price' => (string) $p->get_regular_price(), 'sale_price' => (string) $p->get_sale_price(), 'currency' => get_woocommerce_currency(), 'stock_status' => (string) $p->get_stock_status(), 'stock_quantity' => $p->managing_stock() ? (int) $p->get_stock_quantity() : null, 'gtin' => self::gtin( $p ), 'mpn' => self::mpn( $p ), 'brand' => self::brand( $p ), 'effective_brand' => self::effective_brand( $p ), 'categories' => self::category_names( $p->get_id() ), 'short_description_text' => Util::plain_text( $p->get_short_description() ), 'description_text' => Util::plain_text( $p->get_description() ), 'description_html' => (string) $p->get_description( 'edit' ), 'faq' => self::faq( $p ), 'image_url' => self::image_url( $p ), 'attributes' => (object) self::attributes( $p ), 'modified' => $modified ? $modified->date( 'c' ) : null, 'in_treatment' => Settings::is_treatment( $p->get_id() ), ) + self::v1_fields( $p ); } /** * Paginated catalog. v1: variations are items too (type "variation", parent_id set), like the feed lists them; * only published variations of published variable parents. $variations = false gives the v0.x shape. */ public static function page( int $page, int $per_page, bool $variations = true ): array { $args = array( 'status' => 'publish', 'limit' => $per_page, 'page' => $page, 'paginate' => true, 'orderby' => 'ID', 'order' => 'ASC', ); if ( $variations ) { $args['type'] = array_merge( array_keys( wc_get_product_types() ), array( 'variation' ) ); } $res = wc_get_products( $args ); $items = array(); foreach ( (array) $res->products as $p ) { if ( $p->is_type( 'variation' ) ) { $parent = wc_get_product( $p->get_parent_id() ); if ( ! $parent || 'publish' !== $parent->get_status() || ! $parent->is_type( 'variable' ) ) { continue; } } $n = Util::safe( static fn() => self::normalize( $p ) ); if ( $n ) { $items[] = $n; } } return array( 'total' => (int) $res->total, 'page' => $page, 'per_page' => $per_page, 'items' => $items, ); } }