File "products.php"
Full Path: /home/fresvfqn/waterdamagerestorationandrepairsmithtown.com/wp-content/plugins/surerank/inc/schema/products.php
File size: 20.68 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Products
*
* This file handles functionality for all Products.
*
* @package SureRank
* @since 1.0.0
*/
namespace SureRank\Inc\Schema;
use SureRank\Inc\Functions\Helper;
use SureRank\Inc\Schema\Types\Product;
use SureRank\Inc\Traits\Get_Instance;
/**
* Products
* This class handles functionality for all Products.
*
* @since 1.0.0
*/
class Products {
use Get_Instance;
/**
* Constructor.
*/
public function __construct() {
if ( ! Helper::wc_status() && ! Helper::sc_status() ) {
return;
}
$this->init();
}
/**
* Initialize the class functionality.
*
* @return void
*/
public function init() {
add_filter( 'surerank_schema_types', [ $this, 'add_product_schema_type' ] );
add_filter( 'surerank_default_schemas', [ $this, 'add_product_schema' ] );
add_filter( 'surerank_schema_data', [ $this, 'add_schema_data' ] );
add_action( 'wp_footer', [ $this, 'remove_wc_schema' ], 0 );
add_filter( 'surerank_default_schema_variables', [ $this, 'add_variables' ] );
add_filter( 'surerank_schema_type_data', [ $this, 'schema_type_data' ] );
}
/**
* Add Product schema type to available schema types.
*
* @param array<string, mixed> $schema_types Existing schema types.
* @return array<string, mixed> Modified schema types.
*/
public function add_product_schema_type( $schema_types ) {
if ( Helper::wc_status() || Helper::sc_status() ) {
$schema_types['Product'] = Product::class;
}
return $schema_types;
}
/**
* Add schema data for products.
*
* @param array<string, mixed> $data Existing schema data.
* @return array<string, mixed> Modified schema data.
*/
public function add_schema_data( $data ) {
$post = is_singular() ? get_post( get_queried_object_id() ) : '';
if ( empty( $post ) ) {
return $data;
}
$post_type = get_post_type( $post );
switch ( $post_type ) {
case 'product':
/**
* Get product data from WooCommerce
*/
$product = wc_get_product( $post->ID ) ?? '';
if ( empty( $product ) ) {
return $data;
}
$product_data = $this->get_product_data( $product );
break;
case 'sc_product':
/**
* Get product data from SureCart
*/
$product = get_post_meta( $post->ID, 'product', true ) ?? '';
if ( empty( $product ) ) {
return $data;
}
$product_data = $this->get_surecart_product_data( $product );
/**
* Remove WooCommerce schema from SureCart product
*/
add_filter( 'sc_display_product_json_ld_schema', '__return_false', 10 );
break;
default:
return $data;
}
// Merge product data into schema.
$data['product'] = $product_data;
return $data;
}
/**
* Remove WooCommerce schema from footer.
*
* @return void
*/
public function remove_wc_schema() {
if ( ! Helper::wc_status() ) {
return;
}
remove_action( 'wp_footer', [ WC()->structured_data, 'output_structured_data' ], 10 );
}
/**
* Add variables for product schema.
*
* @param array<string, mixed> $variables Existing variables.
* @return array<string, mixed> Modified variables.
*/
public function add_variables( $variables ) {
$wc_variables = [
'%product.price%' => __( 'Product Price', 'surerank' ),
'%product.price_with_tax%' => __( 'Product Price Including Tax', 'surerank' ),
'%product.low_price%' => __( 'Product Low Price (variable product)', 'surerank' ),
'%product.high_price%' => __( 'Product High Price (variable product)', 'surerank' ),
'%product.offer_count%' => __( 'Product Offer Count (variable product)', 'surerank' ),
'%product.sale_from%' => __( 'Product Sale Price Date "From"', 'surerank' ),
'%product.sale_to%' => __( 'Product Sale Price Date "To"', 'surerank' ),
'%product.sku%' => __( 'Product SKU', 'surerank' ),
'%product.stock%' => __( 'Product Stock Status', 'surerank' ),
'%product.currency%' => __( 'Product Currency', 'surerank' ),
'%product.rating%' => __( 'Product Rating Value', 'surerank' ),
'%product.review_count%' => __( 'Product Review Count', 'surerank' ),
'%product.image%' => __( 'Product Image', 'surerank' ),
'%product.image_width%' => __( 'Product Image Width', 'surerank' ),
'%product.image_height%' => __( 'Product Image Height', 'surerank' ),
'%product.description%' => __( 'Product Description', 'surerank' ),
'%product.variant_sku%' => __( 'Product Variant SKU', 'surerank' ),
'%product.variant_name%' => __( 'Product Variant Name', 'surerank' ),
'%product.variant_url%' => __( 'Product Variant URL', 'surerank' ),
'%product.variant_image%' => __( 'Product Variant Image', 'surerank' ),
'%product.variant_size%' => __( 'Product Variant Size', 'surerank' ),
'%product.variant_color%' => __( 'Product Variant Color', 'surerank' ),
'%product.variant_description%' => __( 'Product Variant Description', 'surerank' ),
'%product.variant_sale_price%' => __( 'Product Variant Sale Price', 'surerank' ),
'%product.variant_regular_price%' => __( 'Product Variant Regular Price', 'surerank' ),
'%product.variant_stock%' => __( 'Product Variant Stock Status', 'surerank' ),
];
return array_merge( $variables, $wc_variables );
}
/**
* Add product schema.
*
* @param array<string, mixed> $schemas Existing schemas.
* @return array<string, mixed> Modified schemas.
*/
public function add_product_schema( $schemas ) {
if ( Helper::wc_status() ) {
$rule = 'product|all';
} elseif ( Helper::sc_status() ) {
$rule = 'sc_product|all';
} else {
return $schemas;
}
$schemas['Product'] = [
'title' => 'Product',
'type' => 'Product',
'show_on' => [
'rules' => [
$rule,
],
'specific' => [],
'specificText' => [],
],
'fields' => Utils::parse_fields( Product::get_instance()->get() ),
];
return $schemas;
}
/**
* Get schema type data.
*
* @param array<string, mixed> $data Existing schema type data.
* @return array<string, mixed> Modified schema type data.
*/
public function schema_type_data( $data ) {
$data['Product'] = Product::get_instance()->get();
return $data;
}
/**
* Get detailed product data for schema.
*
* @param \WC_Product $product WooCommerce product object.
* @return array<string, mixed> Product data for schema.
*/
private function get_product_data( $product ): array {
$basic_data = $this->get_basic_product_data( $product );
$sale_dates = $this->calculate_sale_dates( $product );
$variation_data = $this->get_variation_data( $product );
$stock_data = $this->get_stock_data( $product );
$image_data = $this->get_product_image_dimensions( $product );
$product_data = array_merge(
$basic_data,
$sale_dates,
$variation_data['pricing'],
$stock_data,
[
'image' => $image_data['image'],
'image_width' => $image_data['image_width'],
'image_height' => $image_data['image_height'],
'description' => $product->get_description(),
'short_description' => $product->get_short_description(),
]
);
return $this->merge_variant_data( $product_data, $variation_data['variant'] );
}
/**
* Get basic product data.
*
* @param \WC_Product $product WooCommerce product object.
* @return array<string, mixed> Basic product data.
*/
private function get_basic_product_data( \WC_Product $product ): array {
$price = $product->get_price();
return [
'price' => $price,
'price_with_tax' => wc_get_price_including_tax( $product, [ 'price' => $price ] ),
'sku' => $product->get_sku(),
'currency' => get_woocommerce_currency(),
'rating' => $product->get_average_rating(),
'review_count' => $product->get_review_count(),
];
}
/**
* Calculate sale dates for the product.
*
* @param \WC_Product $product WooCommerce product object.
* @return array<string, string> Sale date information.
*/
private function calculate_sale_dates( \WC_Product $product ): array {
$sale_from = $this->get_sale_from_date( $product );
$sale_to = $this->get_sale_to_date( $product, $sale_from );
return [
'sale_from' => $sale_from,
'sale_to' => $sale_to,
];
}
/**
* Get sale from date.
*
* @param \WC_Product $product WooCommerce product object.
* @return string Sale from date.
*/
private function get_sale_from_date( \WC_Product $product ): string {
$date_on_sale_from = $product->get_date_on_sale_from();
return $date_on_sale_from ? gmdate( 'Y-m-d', $date_on_sale_from->getTimestamp() ) : '';
}
/**
* Get sale to date.
*
* @param \WC_Product $product WooCommerce product object.
* @param string $sale_from Sale from date.
* @return string Sale to date.
*/
private function get_sale_to_date( \WC_Product $product, string $sale_from ): string {
$today = gmdate( 'Y-m-d' );
if ( $product->is_on_sale() && $product->get_date_on_sale_to() ) {
return gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
}
if ( $sale_from > $today ) {
return gmdate( 'Y-m-d', wc_string_to_timestamp( $sale_from ) - DAY_IN_SECONDS );
}
if ( $sale_from === $today ) {
return $today;
}
return gmdate( 'Y-m-d', wc_string_to_timestamp( '+1 month' ) );
}
/**
* Get variation data for variable products.
*
* @param \WC_Product $product WooCommerce product object.
* @return array{pricing: array<string, mixed>, variant: array<string, mixed>} Variation data.
*/
private function get_variation_data( \WC_Product $product ): array {
if ( ! $product->is_type( 'variable' ) || ! $product instanceof \WC_Product_Variable ) {
return [
'pricing' => [
'low_price' => '',
'high_price' => '',
'offer_count' => 0,
],
'variant' => [],
];
}
return $this->process_variable_product( $product );
}
/**
* Process variable product data.
*
* @param \WC_Product_Variable $product Variable product.
* @return array{pricing: array<string, mixed>, variant: array<string, mixed>} Processed data.
*/
private function process_variable_product( \WC_Product_Variable $product ): array {
$pricing_data = $this->get_variable_pricing_data( $product );
$variant_data = $this->get_default_variant_data( $product );
return [
'pricing' => $pricing_data,
'variant' => $variant_data,
];
}
/**
* Get variable product pricing data.
*
* @param \WC_Product_Variable $product Variable product.
* @return array<string, mixed> Pricing data.
*/
private function get_variable_pricing_data( \WC_Product_Variable $product ): array {
return [
'low_price' => wc_get_price_including_tax( $product, [ 'price' => $product->get_variation_price( 'min', false ) ] ),
'high_price' => wc_get_price_including_tax( $product, [ 'price' => $product->get_variation_price( 'max', false ) ] ),
'offer_count' => count( $product->get_children() ),
];
}
/**
* Get default variant data.
*
* @param \WC_Product_Variable $product Variable product.
* @return array<string, mixed> Default variant data.
*/
private function get_default_variant_data( \WC_Product_Variable $product ): array {
$variations = $product->get_available_variations();
if ( empty( $variations ) || ! is_array( $variations ) ) {
return [];
}
$default_variation = $variations[0] ?? [];
if ( ! is_array( $default_variation ) ) {
return [];
}
$variation_id = $default_variation['variation_id'] ?? null;
if ( ! $variation_id ) {
return [];
}
return $this->build_variant_data( $variation_id, $default_variation, $product );
}
/**
* Build variant data from variation.
*
* @param int $variation_id Variation ID.
* @param array<string, mixed> $default_variation Default variation data.
* @param \WC_Product $parent_product Parent product.
* @return array<string, mixed> Variant data.
*/
private function build_variant_data( int $variation_id, array $default_variation, \WC_Product $parent_product ): array {
$variation_obj = wc_get_product( $variation_id );
if ( ! $variation_obj || ! $variation_obj instanceof \WC_Product_Variation ) {
return [];
}
$attributes = $this->extract_variant_attributes( $variation_obj );
$image_data = $this->get_variant_image( $variation_obj, $parent_product );
$stock_data = $this->get_variant_stock( $variation_obj );
return [
'sku' => $variation_obj->get_sku(),
'name' => $variation_obj->get_name(),
'url' => $variation_obj->get_permalink(),
'image' => $image_data,
'size' => $attributes['size'],
'color' => $attributes['color'],
'description' => $default_variation['variation_description'] ?? '',
'sale_price' => $this->get_variant_sale_price( $variation_obj ),
'regular_price' => wc_get_price_including_tax( $variation_obj, [ 'price' => $variation_obj->get_regular_price() ] ),
'stock' => $stock_data,
];
}
/**
* Extract variant attributes.
*
* @param \WC_Product_Variation $variation Variation object.
* @return array{size: string, color: string} Extracted attributes.
*/
private function extract_variant_attributes( \WC_Product_Variation $variation ): array {
$attributes = $variation->get_attributes();
$size = '';
$color = '';
foreach ( $attributes as $attribute_name => $attribute_value ) {
$taxonomy = str_replace( 'attribute_', '', $attribute_name );
if ( strpos( $taxonomy, 'size' ) !== false ) {
$size = $attribute_value;
} elseif ( strpos( $taxonomy, 'color' ) !== false || strpos( $taxonomy, 'colour' ) !== false ) {
$color = $attribute_value;
}
}
return [
'size' => $size,
'color' => $color,
];
}
/**
* Get variant image.
*
* @param \WC_Product_Variation $variation Variation object.
* @param \WC_Product $parent_product Parent product.
* @return string Image URL.
*/
private function get_variant_image( \WC_Product_Variation $variation, \WC_Product $parent_product ): string {
$image_id = $variation->get_image_id() ? $parent_product->get_image_id() : 0;
$image = wp_get_attachment_image_src( (int) $image_id, 'single-post-thumbnail' );
return $image ? $image[0] : '';
}
/**
* Get variant stock status.
*
* @param \WC_Product_Variation $variation Variation object.
* @return string Stock status URL.
*/
private function get_variant_stock( \WC_Product_Variation $variation ): string {
$statuses = $this->get_stock_status_mapping();
$status = strtolower( $variation->get_stock_status() );
return 'https://schema.org/' . ( $statuses[ $status ] ?? 'InStock' );
}
/**
* Get variant sale price.
*
* @param \WC_Product_Variation $variation Variation object.
* @return string Sale price.
*/
private function get_variant_sale_price( \WC_Product_Variation $variation ): string {
$sale_price = $variation->get_sale_price();
return $sale_price ? (string) wc_get_price_including_tax( $variation, [ 'price' => $sale_price ] ) : '';
}
/**
* Get stock data for product.
*
* @param \WC_Product $product Product object.
* @return array<string, string> Stock data.
*/
private function get_stock_data( \WC_Product $product ): array {
$statuses = $this->get_stock_status_mapping();
$status = strtolower( $product->get_stock_status() );
return [
'stock' => 'https://schema.org/' . ( $statuses[ $status ] ?? 'InStock' ),
];
}
/**
* Merge variant data into product data.
*
* @param array<string, mixed> $product_data Base product data.
* @param array<string, mixed> $variant_data Variant data.
* @return array<string, mixed> Merged data.
*/
private function merge_variant_data( array $product_data, array $variant_data ): array {
if ( empty( $variant_data ) ) {
return $product_data;
}
foreach ( $variant_data as $key => $value ) {
$product_data[ 'variant_' . $key ] = $value ?? '';
}
return $product_data;
}
/**
* Get product image dimensions.
*
* @param \WC_Product $product WooCommerce product object.
* @return array<string, mixed> Product image dimensions.
*/
private function get_product_image_dimensions( $product ) {
$image_data = wp_get_attachment_image_src( (int) $product->get_image_id(), 'single-post-thumbnail' );
$product_image_url = $image_data[0] ?? '';
if ( empty( $product_image_url ) ) {
$surerank_settings = get_post_meta( $product->get_id(), SURERANK_SETTINGS, true );
$product_image_url = $surerank_settings['facebook_image_url'] ?? '';
}
return [
'image' => $product_image_url ?? '',
'image_width' => $image_data[1] ?? '',
'image_height' => $image_data[2] ?? '',
];
}
/**
* Get stock status mapping for schema.
*
* @return array<string, mixed> Stock status mapping.
*/
private function get_stock_status_mapping() {
return [
'instock' => 'InStock',
'outofstock' => 'OutOfStock',
'onbackorder' => 'BackOrder',
'soldout' => 'SoldOut',
];
}
/**
* Get detailed product data for schema from SureCart data.
*
* @param array<string, mixed> $post_data SureCart product data array.
* @return array<string, mixed> Product data for schema.
*/
private function get_surecart_product_data( $post_data ) {
$price = isset( $post_data['initial_amount'] ) ? $post_data['initial_amount'] / 100 : '';
$price_with_tax = $price;
// Sale dates (SureCart doesn't provide sale dates directly in this data, so we'll leave them empty unless you have a custom field).
$sale_from = '';
$sale_to = '';
$low_price = isset( $post_data['metrics']['min_price_amount'] ) ? $post_data['metrics']['min_price_amount'] / 100 : $price;
$high_price = isset( $post_data['metrics']['max_price_amount'] ) ? $post_data['metrics']['max_price_amount'] / 100 : $price;
$offer_count = $post_data['variants']['pagination']['count'] ?? 0;
$variant_data = [];
if ( $offer_count > 0 && ! empty( $post_data['variants']['data'] ) ) {
// Get the first variant as the default.
$default_variant = $post_data['variants']['data'][0];
$size = $default_variant['option_1'] ?? '';
$color = $default_variant['option_2'] ?? '';
// Variant image (use product image if variant-specific image is not set).
$variant_image_url = $default_variant['image_url'] ?? $post_data['image_url'];
// Stock status.
$stock_enabled = $post_data['stock_enabled'] ?? false;
$stock_status = $stock_enabled && $default_variant['available_stock'] <= 0 ? 'OutOfStock' : 'InStock';
$var_stock = 'https://schema.org/' . $stock_status;
// Variant data (SureCart doesn't seem to have variant-specific prices here; use initial price).
$variant_data = [
'sku' => $default_variant['sku'] ?? '',
'name' => $post_data['name'] . ' - ' . $size . ' ' . $color,
'url' => $post_data['checkout_permalink'] ?? '',
'image' => $variant_image_url,
'size' => $size,
'color' => $color,
'description' => $post_data['description'] ?? '',
'sale_price' => '',
'regular_price' => $price,
'stock' => $var_stock,
];
}
// Stock status for the main product.
$stock_enabled = $post_data['stock_enabled'] ?? false;
$stock_status = $stock_enabled && $post_data['available_stock'] <= 0 ? 'OutOfStock' : 'InStock';
$stock = 'https://schema.org/' . $stock_status;
// Get product image dimensions from preview image.
$preview_image = $post_data['preview_image'] ?? '';
$image_url = $preview_image->src ?? '';
$image_width = $preview_image->width ?? '';
$image_height = $preview_image->height ?? '';
// Currency (from SureCart's initial price or metrics).
$currency = $post_data['initial_price']['currency'] ?? 'usd';
// Rating and reviews (not available in SureCart data, so default to empty).
$rating = '';
$review_count = '';
// Assemble product data.
$product_data = [
'price' => $price,
'price_with_tax' => $price_with_tax,
'low_price' => $low_price,
'high_price' => $high_price,
'offer_count' => $offer_count,
'sale_from' => $sale_from,
'sale_to' => $sale_to,
'sku' => $post_data['sku'] ?? '',
'stock' => $stock,
'currency' => strtoupper( $currency ),
'rating' => $rating,
'review_count' => $review_count,
'image' => $image_url,
'image_width' => $image_width,
'image_height' => $image_height,
'description' => $post_data['description'] ?? '',
'short_description' => $post_data['meta_description'] ?? '',
];
// Add variant data if available.
if ( ! empty( $variant_data ) ) {
foreach ( $variant_data as $key => $value ) {
$product_data[ 'variant_' . $key ] = $value ?? '';
}
}
return $product_data;
}
}