(string) $product->get_id(), 'name' => $product->get_name(), 'slug' => $product->get_slug(), 'permalink' => $product->get_permalink(), 'sku' => $product->get_sku(), 'price' => (string) $product->get_price(), 'description' => $product->get_description(), 'short_description' => $product->get_short_description(), 'type' => $product->get_type(), 'stock_status' => $product->get_stock_status(), 'manage_stock' => (bool) $product->get_manage_stock(), 'stock_quantity' => $product->get_stock_quantity(), 'images' => $this->images( $product ), 'tags' => $this->term_names( $product->get_id(), 'product_tag' ), 'categories' => $this->term_names( $product->get_id(), 'product_cat' ), 'variations' => $this->variations( $product ), 'meta_data' => $this->meta_data( $product ), // Custom fields as product content (4.24). The scheme tells the // platform what extracted them, so a change of extraction // re-baselines Autopilot rather than reading as a product edit. 'custom_fields' => Custom_Fields::collect( $product, Settings::custom_field_exclusions() ), 'custom_fields_scheme' => Custom_Fields::SCHEME, ); } /** * Featured image first, then gallery, as [{src, alt}]. * * `alt` is the image's own alt text, '' when it has none (4.20.1). Until * then an empty one was sent as the product name, so every undescribed image * reached Rezolve Ai looking described: its review screens showed the name as * the current alt text, and its quality checks counted it as present. * * @param \WC_Product $product Product. * @return array */ private function images( $product ) { $ids = array(); if ( $product->get_image_id() ) { $ids[] = $product->get_image_id(); } foreach ( (array) $product->get_gallery_image_ids() as $gid ) { $ids[] = $gid; } $images = array(); foreach ( array_unique( $ids ) as $id ) { $src = wp_get_attachment_image_url( $id, 'full' ); if ( ! $src ) { continue; } $images[] = array( 'src' => $src, 'alt' => (string) get_post_meta( $id, '_wp_attachment_image_alt', true ), ); } return $images; } /** * Term names for a taxonomy as [{name}]. * * @param int $product_id Product id. * @param string $taxonomy Taxonomy. * @return array */ private function term_names( $product_id, $taxonomy ) { $terms = get_the_terms( $product_id, $taxonomy ); $out = array(); if ( is_array( $terms ) ) { foreach ( $terms as $term ) { if ( isset( $term->name ) && '' !== $term->name ) { $out[] = array( 'name' => $term->name ); } } } return $out; } /** * Child variations for variable products, in full. * * Ids alone were useless to the server: it has no WooCommerce credentials * with which to look the children up, so every variable product reached the * catalogue as a single "Default" variant priced at the parent's minimum. * Price, SKU, stock and the chosen attributes are what make a variant * purchasable, so they travel with the id. * * @param \WC_Product $product Product. * @return array */ private function variations( $product ) { if ( ! $product->is_type( 'variable' ) ) { return array(); } $out = array(); foreach ( (array) $product->get_children() as $child_id ) { if ( count( $out ) >= self::VARIATION_LIMIT ) { break; } $variation = wc_get_product( $child_id ); // A child can be missing, trashed or draft. Publishing one as // purchasable would send an agent to a variation that 404s. if ( ! $variation || ! $variation->is_type( 'variation' ) ) { continue; } if ( 'publish' !== get_post_status( $child_id ) ) { continue; } $out[] = array( 'id' => (string) $variation->get_id(), 'sku' => $variation->get_sku(), 'price' => (string) $variation->get_price(), 'regular_price' => (string) $variation->get_regular_price(), 'stock_status' => $variation->get_stock_status(), 'stock_quantity' => $variation->get_stock_quantity(), 'attributes' => $this->variation_attributes( $variation ), ); } return $out; } /** * A variation's chosen attributes as [{name, option}], in display terms. * * WooCommerce stores the axis as a taxonomy slug and the choice as a term * slug, neither of which is a label. Resolving both here rather than on the * server keeps the translation next to the site that owns the vocabulary. * * @param \WC_Product_Variation $variation Variation. * @return array */ private function variation_attributes( $variation ) { $out = array(); foreach ( (array) $variation->get_variation_attributes( false ) as $name => $option ) { // An empty option means "any": the variation matches every value on // that axis, so there is no single choice to publish. if ( '' === (string) $option ) { continue; } $label = wc_attribute_label( $name, $variation ); $value = $option; if ( taxonomy_exists( $name ) ) { $term = get_term_by( 'slug', $option, $name ); if ( $term && ! is_wp_error( $term ) ) { $value = $term->name; } } $out[] = array( 'name' => (string) $label, 'option' => (string) $value, ); } return $out; } /** * Minimal meta the server actually reads (vendor/brand). Avoids leaking * private post meta and our own _agencoen_* bookkeeping. * * @param \WC_Product $product Product. * @return array */ private function meta_data( $product ) { $wanted = array( 'vendor', '_vendor', 'brand', '_brand' ); $out = array(); foreach ( $wanted as $key ) { $value = $product->get_meta( $key ); if ( is_scalar( $value ) && '' !== (string) $value ) { $out[] = array( 'key' => $key, 'value' => (string) $value, ); } } return $out; } }