WC_Structured_Data::generate_product_datapublicWC 1.0

Generates Product structured data.

Hooked into woocommerce_single_product_summary hook.

Method of the class: WC_Structured_Data{}

Returns

null. Nothing (null).

Usage

$WC_Structured_Data = new WC_Structured_Data();
$WC_Structured_Data->generate_product_data( $product );
$product(WC_Product)
Product data .
Default: null)

WC_Structured_Data::generate_product_data() code WC 10.6.2

public function generate_product_data( $product = null ) {
	if ( ! is_object( $product ) ) {
		global $product;
	}

	if ( ! is_a( $product, 'WC_Product' ) ) {
		return;
	}

	$shop_name = get_bloginfo( 'name' );
	$shop_url  = home_url();
	$currency  = get_woocommerce_currency();
	$permalink = get_permalink( $product->get_id() );
	$image     = wp_get_attachment_url( $product->get_image_id() );

	$markup = array(
		'@type'       => 'Product',
		'@id'         => $permalink . '#product', // Append '#product' to differentiate between this @id and the @id generated for the Breadcrumblist.
		'name'        => wp_kses_post( $product->get_name() ),
		'url'         => $permalink,
		'description' => wp_strip_all_tags( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) ),
	);

	if ( $image ) {
		$markup['image'] = $image;
	}

	// Declare SKU or fallback to ID.
	if ( $product->get_sku() ) {
		$markup['sku'] = $product->get_sku();
	} else {
		$markup['sku'] = $product->get_id();
	}

	// Prepare GTIN and load it if it's valid.
	$gtin = $this->prepare_gtin( $product->get_global_unique_id() );
	if ( $this->is_valid_gtin( $gtin ) ) {
		$markup['gtin'] = $gtin;
	}

	if ( '' !== $product->get_price() ) {
		// Assume prices will be valid until the end of next year, unless on sale and there is an end date.
		$price_valid_until = gmdate( 'Y-12-31', time() + YEAR_IN_SECONDS );

		if ( $product->is_type( ProductType::VARIABLE ) ) {
			$lowest  = $product->get_variation_price( 'min', true );
			$highest = $product->get_variation_price( 'max', true );

			$variation_prices = $product->get_variation_prices( true );

			if ( $lowest === $highest ) {
				$markup_offer = array(
					'@type'              => 'Offer',
					'priceSpecification' => array(
						array(
							'@type'                 => 'UnitPriceSpecification',
							'price'                 => wc_format_decimal( $lowest, wc_get_price_decimals() ),
							'priceCurrency'         => $currency,
							'valueAddedTaxIncluded' => 'incl' === get_option( 'woocommerce_tax_display_shop' ),
							'validThrough'          => $price_valid_until,
						),
					),
				);
			} else {
				$markup_offer = array(
					'@type'      => 'AggregateOffer',
					'lowPrice'   => wc_format_decimal( $lowest, wc_get_price_decimals() ),
					'highPrice'  => wc_format_decimal( $highest, wc_get_price_decimals() ),
					'offerCount' => count( $variation_prices['price'] ),
				);

				if ( $product->is_on_sale() ) {
					$lowest_child_sale_price = $product->get_variation_sale_price( 'min', true );
					foreach ( $variation_prices['sale_price'] as $variation_id => $variation_price ) {
						if ( $variation_price === $lowest_child_sale_price ) {
							break;
						}
					}
					$date_on_sale_to        = isset( $variation_id )
						? wc_get_product( $variation_id )->get_date_on_sale_to()
						: null;
					$sale_price_valid_until = $date_on_sale_to
						? gmdate( 'Y-m-d', $date_on_sale_to->getTimestamp() )
						: null;

					$markup_offer['priceSpecification'] = array(
						array(
							'@type'                 => 'UnitPriceSpecification',
							'priceType'             => 'https://schema.org/SalePrice',
							'price'                 => wc_format_decimal( $lowest_child_sale_price, wc_get_price_decimals() ),
							'priceCurrency'         => $currency,
							'valueAddedTaxIncluded' => 'incl' === get_option( 'woocommerce_tax_display_shop' ),
							'validThrough'          => $sale_price_valid_until ?? $price_valid_until,
						),
					);
				}
			}
		} elseif ( $product->is_type( ProductType::GROUPED ) ) {
			$tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
			$children         = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' );
			$price_function   = 'incl' === $tax_display_mode ? 'wc_get_price_including_tax' : 'wc_get_price_excluding_tax';

			foreach ( $children as $child ) {
				if ( '' !== $child->get_regular_price() ) {
					$child_prices[] = $price_function( $child, array( 'price' => $child->get_regular_price() ) );
				}
				if ( '' !== $child->get_sale_price() ) {
					$child_sale_prices[] = $price_function( $child, array( 'price' => $child->get_sale_price() ) );
				}
			}
			if ( empty( $child_prices ) ) {
				$min_price = 0;
			} else {
				$min_price = min( $child_prices );
			}
			if ( empty( $child_sale_prices ) ) {
				$min_sale_price = 0;
			} else {
				$min_sale_price = min( $child_sale_prices );
			}

			$unit_price_specification = array(
				'@type'                 => 'UnitPriceSpecification',
				'price'                 => wc_format_decimal( $min_price, wc_get_price_decimals() ),
				'priceCurrency'         => $currency,
				'valueAddedTaxIncluded' => 'incl' === $tax_display_mode,
				'validThrough'          => $price_valid_until,
			);
			if ( $product->is_on_sale() && $min_price !== $min_sale_price ) {
				// `priceType` should only be specified in prices which are not the current offer.
				// https://developers.google.com/search/docs/appearance/structured-data/merchant-listing#sale-pricing-example
				$unit_price_specification['priceType'] = 'https://schema.org/ListPrice';
			}
			$markup_offer = array(
				'@type'              => 'Offer',
				'priceSpecification' => array(
					$unit_price_specification,
				),
			);

			if ( $product->is_on_sale() && $min_price !== $min_sale_price ) {
				if ( $product->get_date_on_sale_to() ) {
					$sale_price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
				}

				// We add the sale price to the top of the array so it's the first offer.
				// See https://github.com/woocommerce/woocommerce/issues/55043.
				array_unshift(
					$markup_offer['priceSpecification'],
					array(
						'@type'                 => 'UnitPriceSpecification',
						'price'                 => wc_format_decimal( $min_sale_price, wc_get_price_decimals() ),
						'priceCurrency'         => $currency,
						'valueAddedTaxIncluded' => 'incl' === $tax_display_mode,
						'validThrough'          => $sale_price_valid_until ?? $price_valid_until,
					)
				);
			}
		} else {
			$tax_display_mode         = get_option( 'woocommerce_tax_display_shop' );
			$regular_price            = 'incl' === $tax_display_mode
				? wc_get_price_including_tax( $product, array( 'price' => $product->get_regular_price() ) )
				: wc_get_price_excluding_tax( $product, array( 'price' => $product->get_regular_price() ) );
			$unit_price_specification = array(
				'@type'                 => 'UnitPriceSpecification',
				'price'                 => wc_format_decimal( $regular_price, wc_get_price_decimals() ),
				'priceCurrency'         => $currency,
				'valueAddedTaxIncluded' => 'incl' === $tax_display_mode,
				'validThrough'          => $price_valid_until,
			);
			if ( $product->is_on_sale() ) {
				// `priceType` should only be specified in prices which are not the current offer.
				// https://developers.google.com/search/docs/appearance/structured-data/merchant-listing#sale-pricing-example
				$unit_price_specification['priceType'] = 'https://schema.org/ListPrice';
			}
			$markup_offer = array(
				'@type'              => 'Offer',
				'priceSpecification' => array(
					$unit_price_specification,
				),
			);

			if ( $product->is_on_sale() ) {
				$sale_price = 'incl' === $tax_display_mode
					? wc_get_price_including_tax( $product, array( 'price' => $product->get_sale_price() ) )
					: wc_get_price_excluding_tax( $product, array( 'price' => $product->get_sale_price() ) );
				if ( $product->get_date_on_sale_to() ) {
					$sale_price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
				}

				// We add the sale price to the top of the array so it's the first offer.
				// See https://github.com/woocommerce/woocommerce/issues/55043.
				array_unshift(
					$markup_offer['priceSpecification'],
					array(
						'@type'                 => 'UnitPriceSpecification',
						'price'                 => wc_format_decimal( $sale_price, wc_get_price_decimals() ),
						'priceCurrency'         => $currency,
						'valueAddedTaxIncluded' => 'incl' === $tax_display_mode,
						'validThrough'          => $sale_price_valid_until ?? $price_valid_until,
					)
				);
			}
		}

		if ( $product->is_in_stock() ) {
			$stock_status_schema = ( ProductStockStatus::ON_BACKORDER === $product->get_stock_status() ) ? 'BackOrder' : 'InStock';
		} else {
			$stock_status_schema = 'OutOfStock';
		}

		$markup_offer += array(
			'priceValidUntil' => $sale_price_valid_until ?? $price_valid_until,
			'availability'    => 'https://schema.org/' . $stock_status_schema,
			'url'             => $permalink,
			'seller'          => array(
				'@type' => 'Organization',
				'name'  => $shop_name,
				'url'   => $shop_url,
			),
		);
		if (
			( ! empty( $markup_offer['price'] ) ||
				! empty( $markup_offer['lowPrice'] ) ||
				! empty( $markup_offer['highPrice'] )
			) && empty( $markup_offer['priceCurrency'] )
		) {
			$markup_offer['priceCurrency'] = $currency;
		}

		$markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) );
	}

	if ( $product->get_rating_count() && wc_review_ratings_enabled() ) {
		$markup['aggregateRating'] = array(
			'@type'       => 'AggregateRating',
			'ratingValue' => $product->get_average_rating(),
			'reviewCount' => $product->get_review_count(),
		);

		// Markup 5 most recent rating/review.
		$comments = get_comments(
			array(
				'number'      => 5,
				'post_id'     => $product->get_id(),
				'status'      => 'approve',
				'post_status' => 'publish',
				'post_type'   => 'product',
				'parent'      => 0,
				'meta_query'  => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					array(
						'key'     => 'rating',
						'type'    => 'NUMERIC',
						'compare' => '>',
						'value'   => 0,
					),
				),
			)
		);

		if ( $comments ) {
			$markup['review'] = array();
			foreach ( $comments as $comment ) {
				$markup['review'][] = array(
					'@type'         => 'Review',
					'reviewRating'  => array(
						'@type'       => 'Rating',
						'bestRating'  => '5',
						'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ),
						'worstRating' => '1',
					),
					'author'        => array(
						'@type' => 'Person',
						'name'  => get_comment_author( $comment ),
					),
					'reviewBody'    => get_comment_text( $comment ),
					'datePublished' => get_comment_date( 'c', $comment ),
				);
			}
		}
	}

	// Check we have required data.
	if ( empty( $markup['aggregateRating'] ) && empty( $markup['offers'] ) && empty( $markup['review'] ) ) {
		return;
	}

	$this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) );
}