/home/bonphmya/liebeszauber-magie.de/wp-content/plugins/gtm-kit/src/Integration/WooCommerce.php
<?php
/**
* WooCommerce.
*
* @see https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?hl=en&client_type=gtm
* @package GTM Kit
*/
namespace TLA_Media\GTM_Kit\Integration;
use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore;
use Automattic\WooCommerce\StoreApi\StoreApi;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Automattic\WooCommerce\StoreApi\Schemas\V1\CartItemSchema;
use Automattic\WooCommerce\StoreApi\Schemas\V1\ProductSchema;
use Exception;
use TLA_Media\GTM_Kit\Common\Conditionals\BricksConditional;
use TLA_Media\GTM_Kit\Common\RestAPIServer;
use TLA_Media\GTM_Kit\Common\Util;
use TLA_Media\GTM_Kit\Options\Options;
use WC_Coupon;
use WC_Customer;
use WC_Order;
use WC_Order_Item_Product;
use WC_Product;
/**
* WooCommerce integration
*/
final class WooCommerce extends AbstractEcommerce {
/**
* Instance.
*
* @var null|WooCommerce
*/
protected static ?WooCommerce $instance = null;
/**
* Stores Rest Extending instance.
*
* @var ExtendSchema
*/
private $extend;
/**
* Constructor.
*
* @param Options $options An instance of Options.
* @param Util $util An instance of Util.
*/
public function __construct( Options $options, Util $util ) {
$this->store_currency = get_woocommerce_currency();
// @phpstan-ignore-next-line
$this->extend = StoreApi::container()->get( ExtendSchema::class );
// Call parent constructor.
parent::__construct( $options, $util );
}
/**
* Get instance
*/
public static function instance(): WooCommerce {
if ( is_null( self::$instance ) ) {
$options = new Options();
$rest_api_server = new RestAPIServer();
$util = new Util( $options, $rest_api_server );
self::$instance = new self( $options, $util );
}
return self::$instance;
}
/**
* Register frontend
*
* @param Options $options An instance of Options.
* @param Util $util An instance of Util.
*/
public static function register( Options $options, Util $util ): void {
self::$instance = new self( $options, $util );
add_filter( 'gtmkit_header_script_settings', [ self::$instance, 'get_global_settings' ] );
add_filter( 'gtmkit_header_script_data', [ self::$instance, 'get_global_data' ] );
add_filter( 'gtmkit_datalayer_content', [ self::$instance, 'get_datalayer_content' ] );
add_action( 'wp_enqueue_scripts', [ self::$instance, 'enqueue_scripts' ] );
// Add-to-cart tracking.
add_action(
'woocommerce_after_add_to_cart_button',
[
self::$instance,
'single_product_add_to_cart_tracking',
]
);
add_filter(
'woocommerce_grouped_product_list_column_label',
[
self::$instance,
'grouped_product_add_to_cart_tracking',
],
10,
2
);
add_filter(
'woocommerce_blocks_product_grid_item_html',
[
self::$instance,
'product_block_add_to_cart_tracking',
],
20,
3
);
add_action( 'woocommerce_after_shop_loop_item', [ self::$instance, 'product_list_loop_add_to_cart_tracking' ] );
add_filter( 'woocommerce_cart_item_remove_link', [ self::$instance, 'cart_item_remove_link' ], 10, 2 );
// Set list name in WooCommerce loop.
add_filter( 'woocommerce_product_loop_start', [ self::$instance, 'set_list_name_on_category_and_tag' ] );
add_filter( 'woocommerce_related_products_columns', [ self::$instance, 'set_list_name_in_woocommerce_loop_filter' ] );
add_filter( 'woocommerce_cross_sells_columns', [ self::$instance, 'set_list_name_in_woocommerce_loop_filter' ] );
add_filter( 'woocommerce_upsells_columns', [ self::$instance, 'set_list_name_in_woocommerce_loop_filter' ] );
add_action(
'woocommerce_shortcode_before_best_selling_products_loop',
[
self::$instance,
'set_list_name_in_woocommerce_loop',
]
);
add_filter(
'safe_style_css',
function ( $styles ) {
$styles[] = 'display';
$styles[] = 'visibility';
return $styles;
}
);
if ( $options->get( 'integrations', 'woocommerce_custom_order_received_page_enabled' ) ) {
add_filter( 'woocommerce_is_order_received_page', [ self::$instance, 'is_custom_order_received_page' ] );
}
add_action(
'woocommerce_shortcode_before_featured_products_loop',
[
self::$instance,
'set_list_name_in_woocommerce_loop',
]
);
add_action(
'woocommerce_shortcode_before_recent_products_loop',
[
self::$instance,
'set_list_name_in_woocommerce_loop',
]
);
add_action(
'woocommerce_shortcode_before_related_products_loop',
[
self::$instance,
'set_list_name_in_woocommerce_loop',
]
);
add_action(
'woocommerce_shortcode_before_sale_products_loop',
[
self::$instance,
'set_list_name_in_woocommerce_loop',
]
);
add_action(
'woocommerce_shortcode_before_top_rated_products_loop',
[
self::$instance,
'set_list_name_in_woocommerce_loop',
]
);
add_action(
'woocommerce_shortcode_before_product_category_loop',
[
self::$instance,
'set_list_name_in_woocommerce_loop',
]
);
add_action( 'woocommerce_blocks_loaded', [ self::$instance, 'extend_store' ] );
}
/**
* Enqueue scripts
*/
public function enqueue_scripts(): void {
if ( $this->options->get( 'integrations', 'woocommerce_dequeue_script' ) ) {
return;
}
$this->util->enqueue_script( 'gtmkit-woocommerce', 'integration/woocommerce.js', false, [ 'jquery' ] );
if ( is_cart() || is_checkout() ) {
if ( has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ) ) {
wp_dequeue_script( 'gtmkit-woocommerce' );
$this->util->enqueue_script( 'gtmkit-woocommerce-blocks', 'frontend/woocommerce-blocks.js', true );
wp_localize_script(
'gtmkit-woocommerce-blocks',
'gtmkitWooCommerceBlocksBuild',
[
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
]
);
} else {
$this->util->enqueue_script( 'gtmkit-woocommerce-checkout', 'integration/woocommerce-checkout.js', false, [ 'gtmkit-woocommerce' ] );
}
} elseif ( has_block( 'woocommerce/all-products' ) || has_block( 'woocommerce/product-collection' ) ) {
$this->util->enqueue_script( 'gtmkit-woocommerce-blocks', 'frontend/woocommerce-blocks.js', true );
wp_localize_script(
'gtmkit-woocommerce-blocks',
'gtmkitWooCommerceBlocksBuild',
[
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
]
);
}
}
/**
* Get the global script settings
*
* @param array<string, mixed> $global_settings Script settings.
*
* @return array<string, mixed>
*/
public function get_global_settings( array $global_settings ): array {
$global_settings['wc']['use_sku'] = (bool) $this->options->get( 'integrations', 'woocommerce_use_sku' );
$global_settings['wc']['pid_prefix'] = $this->prefix_item_id();
$global_settings['wc']['add_shipping_info']['config'] = (int) $this->options->get( 'integrations', 'woocommerce_shipping_info' );
$global_settings['wc']['add_payment_info']['config'] = (int) $this->options->get( 'integrations', 'woocommerce_payment_info' );
$global_settings['wc']['view_item']['config'] = (int) $this->options->get( 'integrations', 'woocommerce_variable_product_tracking' );
$global_settings['wc']['view_item_list']['config'] = (int) $this->options->get( 'integrations', 'woocommerce_view_item_list_limit' );
$global_settings['wc']['wishlist'] = false;
$global_settings['wc']['css_selectors'] = $this->get_css_selectors();
$global_settings['wc']['text'] = [
'wp-block-handpicked-products' => __( 'Handpicked Products', 'gtm-kit' ),
'wp-block-product-best-sellers' => __( 'Best Sellers', 'gtm-kit' ),
'wp-block-product-category' => __( 'Product Category', 'gtm-kit' ),
'wp-block-product-new' => __( 'New Products', 'gtm-kit' ),
'wp-block-product-on-sale' => __( 'Products On Sale', 'gtm-kit' ),
'wp-block-products-by-attribute' => __( 'Products By Attribute', 'gtm-kit' ),
'wp-block-product-tag' => __( 'Product Tag', 'gtm-kit' ),
'wp-block-product-top-rated' => __( 'Top Rated Products', 'gtm-kit' ),
'shipping-tier-not-found' => __( 'Shipping tier not found', 'gtm-kit' ),
'payment-method-not-found' => __( 'Payment method not found', 'gtm-kit' ),
];
return $global_settings;
}
/**
* Get CSS Selectors
*
* @return array{product_list_select_item: string, product_list_element: string, product_list_exclude: string, product_list_add_to_cart: string}
*/
private function get_css_selectors(): array {
$css_selectors = [
'product_list_select_item' => '.products .product:not(.product-category) a:not(.add_to_cart_button.ajax_add_to_cart,.add_to_wishlist),' .
'.wc-block-grid__products li:not(.product-category) a:not(.add_to_cart_button.ajax_add_to_cart,.add_to_wishlist),' .
'.woocommerce-grouped-product-list-item__label a:not(.add_to_wishlist)',
'product_list_element' => '.product,.wc-block-grid__product',
'product_list_exclude' => '',
'product_list_add_to_cart' => '.add_to_cart_button.ajax_add_to_cart:not(.single_add_to_cart_button)',
];
if ( ( new BricksConditional() )->is_met() ) {
$css_selectors['product_list_add_to_cart'] .= ',.add_to_cart_button.brx_ajax_add_to_cart:not(.single_add_to_cart_button)';
}
return $css_selectors;
}
/**
* Get the global script data
*
* @param array<string, mixed> $global_data Script data.
*
* @return array<string, mixed>
*/
public function get_global_data( array $global_data ): array {
$global_data['wc']['currency'] = $this->store_currency;
$global_data['wc']['is_cart'] = is_cart();
$global_data['wc']['is_checkout'] = ( is_checkout() && ! is_order_received_page() );
$global_data['wc']['blocks'] = $this->get_woocommerce_blocks();
if ( is_cart() ) {
$global_data['wc']['cart_items'] = $this->get_cart_items( 'view_cart' );
}
if ( is_checkout() && ! is_order_received_page() ) {
$global_data['wc']['cart_items'] = $this->get_cart_items( 'begin_checkout' );
$global_data['wc']['cart_value'] = round( wc_prices_include_tax() ? ( WC()->cart->get_cart_contents_total() + WC()->cart->get_cart_contents_tax() ) : WC()->cart->get_cart_contents_total(), 2 );
$global_data['wc']['chosen_shipping_method'] = WC()->session->get( 'chosen_shipping_methods' )[0] ?? '';
$global_data['wc']['chosen_payment_method'] = $this->get_payment_method();
$global_data['wc']['add_payment_info']['fired'] = false;
$global_data['wc']['add_shipping_info']['fired'] = false;
}
$this->global_data = $global_data;
return $global_data;
}
/**
* Get the payment method
*
* @return string|null
*/
private function get_payment_method(): ?string {
$payment_method = WC()->session->get( 'chosen_payment_method' );
if ( ! $payment_method ) {
$payment_method = array_key_first( WC()->payment_gateways()->get_available_payment_gateways() );
}
return $payment_method;
}
/**
* Get the WooCommerce dataLayer content
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
public function get_datalayer_content( array $data_layer ): array {
if ( is_product() ) {
$data_layer = $this->get_datalayer_content_product_page( $data_layer );
} elseif ( is_product_category() ) {
$data_layer = $this->get_datalayer_content_product_category( $data_layer );
} elseif ( is_product_tag() ) {
$data_layer = $this->get_datalayer_content_product_tag( $data_layer );
} elseif ( is_order_received_page() ) {
$data_layer = $this->get_datalayer_content_order_received( $data_layer );
} elseif ( is_checkout() ) {
$data_layer = $this->get_datalayer_content_checkout( $data_layer );
} elseif ( is_cart() ) {
$data_layer = $this->get_datalayer_content_cart( $data_layer );
}
if ( $this->options->get( 'integrations', 'woocommerce_include_permalink_structure' ) ) {
$data_layer = $this->get_permalink_structure_property( $data_layer );
}
if ( $this->options->get( 'integrations', 'woocommerce_include_pages' ) ) {
$data_layer = $this->get_pages_property( $data_layer );
}
return $data_layer;
}
/**
* Get the dataLayer data for product pages
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
public function get_datalayer_content_product_page( array $data_layer ): array {
$product = wc_get_product( get_the_ID() );
if ( ! ( $product instanceof WC_Product ) ) {
return $data_layer;
}
if ( $this->options->get( 'general', 'datalayer_page_type' ) ) {
$data_layer['pageType'] = 'product-page';
}
if ( $product->get_type() === 'variable' && (int) $this->options->get( 'integrations', 'woocommerce_variable_product_tracking' ) === 2 ) {
return $data_layer;
}
$item = $this->get_item_data( $product );
$data_layer['productType'] = $product->get_type();
$data_layer['event'] = 'view_item';
$data_layer['ecommerce'] = [
'items' => [ $item ],
'value' => round( $item['price'], 2 ),
'currency' => $this->store_currency,
];
return $data_layer;
}
/**
* Get the dataLayer data for category pages
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
public function get_datalayer_content_product_category( array $data_layer ): array {
if ( $this->options->get( 'general', 'datalayer_page_type' ) ) {
$data_layer['pageType'] = 'product-category';
}
return $data_layer;
}
/**
* Get the dataLayer data for product tag pages
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
public function get_datalayer_content_product_tag( array $data_layer ): array {
if ( $this->options->get( 'general', 'datalayer_page_type' ) ) {
$data_layer['pageType'] = 'product-tag';
}
return $data_layer;
}
/**
* Get the dataLayer data for cart page
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
public function get_datalayer_content_cart( array $data_layer ): array {
if ( wc_prices_include_tax() ) {
$cart_value = WC()->cart->get_cart_contents_total() + WC()->cart->get_cart_contents_tax();
} else {
$cart_value = WC()->cart->get_cart_contents_total();
}
if ( $this->options->get( 'general', 'datalayer_page_type' ) ) {
$data_layer['pageType'] = 'cart';
}
$data_layer['event'] = 'view_cart';
$data_layer['ecommerce'] = [
'currency' => $this->store_currency,
'value' => round( $cart_value, 2 ),
'items' => $this->get_cart_items( 'view_cart' ),
];
return $data_layer;
}
/**
* Get the dataLayer data for checkout page
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
public function get_datalayer_content_checkout( array $data_layer ): array {
if ( $this->options->get( 'general', 'datalayer_page_type' ) ) {
$data_layer['pageType'] = 'checkout';
}
if ( wc_prices_include_tax() ) {
$cart_value = WC()->cart->get_cart_contents_total() + WC()->cart->get_cart_contents_tax();
} else {
$cart_value = WC()->cart->get_cart_contents_total();
}
$data_layer['event'] = 'begin_checkout';
$data_layer['ecommerce']['currency'] = $this->store_currency;
$data_layer['ecommerce']['value'] = round( $cart_value, 2 );
$coupons = WC()->cart->get_applied_coupons();
if ( $coupons ) {
$data_layer['ecommerce']['coupon'] = implode( '|', array_filter( $coupons ) );
}
$data_layer['ecommerce']['items'] = $this->global_data['wc']['cart_items'];
return $data_layer;
}
/**
* Get the dataLayer data for order_received page
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
public function get_datalayer_content_order_received( array $data_layer ): array {
global $wp;
$order_id = apply_filters( 'woocommerce_thankyou_order_id', absint( $wp->query_vars['order-received'] ?? 0 ) );
if ( ! $order_id || apply_filters( 'gtmkit_disable_frontend_purchase_event', false ) ) {
return $data_layer;
}
$order = wc_get_order( $order_id );
if ( ! ( $order instanceof WC_Order ) ) {
return $data_layer;
}
$order_key = apply_filters( 'woocommerce_thankyou_order_key', empty( $_GET['key'] ) ? '' : wc_clean( wp_unslash( $_GET['key'] ) ) ); // phpcs:ignore
if ( $order->get_order_key() !== $order_key ) {
return $data_layer;
}
if ( ( 'failed' === $order->get_status() ) ) {
return $data_layer;
}
if ( apply_filters( 'gtmkit_datalayer_exit_order_received', false, $order ) ) {
return $data_layer;
}
if ( ( 1 === (int) $order->get_meta( '_gtmkit_order_tracked' ) ) ) {
if ( ! ( $this->options->is_const_defined( 'integrations', 'woocommerce_debug_track_purchase' ) ) ) {
return $data_layer;
} else {
$data_layer['debug'] = 'order-already-tracked';
}
if ( $this->options->get( 'general', 'debug_log' ) ) {
$logger = wc_get_logger();
$logger->info( 'Order already tracked: ' . $order->get_id(), [ 'source' => 'gtmkit-order-already-tracked' ] );
}
}
if ( $this->options->get( 'general', 'datalayer_page_type' ) ) {
$data_layer['pageType'] = 'order-received';
}
if ( $this->options->get( 'integrations', 'woocommerce_exclude_tax' ) ) {
$order_value = $order->get_total() - $order->get_total_tax();
} else {
$order_value = $order->get_total();
}
$data_layer = $this->get_purchase_event( $order, $data_layer );
if ( $this->options->get( 'integrations', 'woocommerce_include_customer_data' ) ) {
$data_layer = $this->include_customer_data( $data_layer, $order_value );
}
$order->add_meta_data( '_gtmkit_order_tracked', '1' );
$order->save();
return apply_filters( 'gtmkit_datalayer_content_order_received', $data_layer );
}
/**
* Retrieves purchase event data for the data layer.
*
* @param WC_Order $order The order.
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content.
*/
public function get_purchase_event( WC_Order $order, array $data_layer = [] ): array {
if ( $this->options->get( 'integrations', 'woocommerce_exclude_tax' ) ) {
$order_value = $order->get_total() - $order->get_total_tax();
} else {
$order_value = $order->get_total();
}
$shipping_total = (float) $order->get_shipping_total();
if ( $this->options->get( 'integrations', 'woocommerce_exclude_shipping' ) ) {
$order_value -= $shipping_total;
}
$data_layer['event'] = 'purchase';
$data_layer['ecommerce'] = [
'transaction_id' => (string) $order->get_order_number(),
'value' => round( $order_value, 2 ),
'tax' => round( $order->get_total_tax(), 2 ),
'shipping' => round( $shipping_total, 2 ),
'currency' => $order->get_currency(),
];
/** @phpstan-ignore-next-line level5 */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
$data_layer['new_customer'] = ! DataStore::is_returning_customer( $order ); // Param $order is declared as array but object is expected.
$coupons = $order->get_coupon_codes();
if ( $coupons ) {
$data_layer['ecommerce']['coupon'] = implode( '|', array_filter( $coupons ) );
}
$data_layer['ecommerce']['items'] = $this->get_order_items( $order );
if ( $this->options->get( 'general', 'debug_log' ) ) {
$logger = wc_get_logger();
$logger->info( wc_print_r( $data_layer, true ), [ 'source' => 'gtmkit-purchase' ] );
}
return $data_layer;
}
/**
* Get the permalinkStructure property for the dataLayer
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
private function get_permalink_structure_property( array $data_layer ): array {
$wc_permalink_structure = \wc_get_permalink_structure();
$data_layer['permalinkStructure'] = [
'productBase' => $wc_permalink_structure['product_base'],
'categoryBase' => $wc_permalink_structure['category_base'],
'tagBase' => $wc_permalink_structure['tag_base'],
'attributeBase' => $wc_permalink_structure['attribute_base'],
];
return $data_layer;
}
/**
* Get the pages property for the dataLayer
*
* @param array<string, mixed> $data_layer The datalayer content.
*
* @return array<string, mixed> The datalayer content
*/
public function get_pages_property( array $data_layer ): array {
$data_layer['pages'] = [
'cart' => str_replace( \home_url(), '', \wc_get_cart_url() ),
'checkout' => str_replace( \home_url(), '', \wc_get_checkout_url() ),
'orderReceived' => str_replace( \home_url(), '', \wc_get_endpoint_url( 'order-received', '', \wc_get_checkout_url() ) ),
'myAccount' => str_replace( \home_url(), '', \get_permalink( \wc_get_page_id( 'myaccount' ) ) ),
];
return $data_layer;
}
/**
* Get cart items.
*
* @param string $event_context The event context of the item data.
*
* @return array<int, mixed> The cart items.
*/
public function get_cart_items( string $event_context ): array {
$cart_items = [];
$coupons = WC()->cart->get_applied_coupons();
foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
$item_data = [
'product_id' => $cart_item['product_id'],
'quantity' => $cart_item['quantity'],
'total' => $cart_item['line_total'],
'total_tax' => $cart_item['line_tax'],
'subtotal' => $cart_item['line_subtotal'],
'subtotal_tax' => $cart_item['line_subtotal_tax'],
];
$coupon_discount = $this->get_coupon_discount( $coupons, $item_data );
$additional_item_attributes = [
'quantity' => $cart_item['quantity'],
];
if ( $coupon_discount['coupon_codes'] ) {
$additional_item_attributes['coupon'] = implode( '|', array_filter( $coupon_discount['coupon_codes'] ) );
}
if ( $coupon_discount['discount'] ) {
$additional_item_attributes['discount'] = round( (float) $coupon_discount['discount'], 2 );
}
$product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
$cart_items[] = $this->get_item_data( $product, $additional_item_attributes, $event_context );
}
return $cart_items;
}
/**
* Get item data.
*
* @param WC_Product $product An instance of WP_Product.
* @param array<string, mixed> $additional_item_attributes Any key-value pair that needs to be added to the item data.
* @param string $event_context The event context of the item data.
*
* @return array<string, mixed> The item data.
*/
public function get_item_data( $product, array $additional_item_attributes = [], string $event_context = '' ): array {
if ( ! ( $product instanceof WC_Product ) ) {
return [];
}
$product_id_to_query = ( $product->get_type() === 'variation' ) ? $product->get_parent_id() : $product->get_id();
if ( $this->options->get( 'integrations', 'woocommerce_use_sku' ) ) {
$item_id = $product->get_sku() ? $product->get_sku() : $product->get_id();
} else {
$item_id = $product->get_id();
}
$item_data = [
'id' => $this->prefix_item_id( $item_id ),
'item_id' => $this->prefix_item_id( $item_id ),
'item_name' => $product->get_title(),
'currency' => $this->store_currency,
'price' => round( (float) wc_get_price_to_display( $product ), 2 ),
];
if ( $this->options->get( 'integrations', 'woocommerce_brand' ) ) {
$item_data['item_brand'] = $this->get_product_term( $product_id_to_query, $this->options->get( 'integrations', 'woocommerce_brand' ) );
}
if ( $this->options->get( 'integrations', 'woocommerce_google_business_vertical' ) ) {
$item_data['google_business_vertical'] = $this->options->get( 'integrations', 'woocommerce_google_business_vertical' );
}
$item_category_elements = $this->get_primary_product_category( $product_id_to_query, 'product_cat' );
$number_of_elements = count( $item_category_elements );
if ( $number_of_elements ) {
for ( $element = 0; $element < $number_of_elements; $element++ ) {
$designator = ( $element === 0 ) ? '' : $element + 1;
$item_data[ 'item_category' . $designator ] = $item_category_elements[ $element ];
}
}
if ( $product->get_type() === 'variation' ) {
$item_data['item_variant'] = implode( ',', array_filter( $product->get_attributes() ) );
}
$item_data = array_merge( $item_data, $additional_item_attributes );
return apply_filters( 'gtmkit_datalayer_item_data', $item_data, $product, $event_context );
}
/**
* Get the coupons and discount for an item
*
* @param array<int, mixed> $coupons The coupons.
* @param array<string, mixed> $item The item.
*
* @return array<string, mixed>
*/
public function get_coupon_discount( array $coupons, array $item ): array {
$discount = 0;
$coupon_codes = [];
if ( $coupons ) {
foreach ( $coupons as $coupon ) {
$coupon = new WC_Coupon( $coupon );
$included_products = true;
$included_cats = true;
$product_ids = $coupon->get_product_ids();
if ( count( $product_ids ) > 0 ) {
if ( ! in_array( $item['product_id'], $product_ids, true ) ) {
$included_products = false;
}
}
$excluded_product_ids = $coupon->get_excluded_product_ids();
if ( count( $excluded_product_ids ) > 0 ) {
if ( in_array( $item['product_id'], $excluded_product_ids, true ) ) {
$included_products = false;
}
}
$product_cats = $coupon->get_product_categories();
if ( count( $product_cats ) > 0 ) {
if ( ! has_term( $product_cats, 'product_cat', $item['product_id'] ) ) {
$included_cats = false;
}
}
$excluded_product_cats = $coupon->get_excluded_product_categories();
if ( count( $excluded_product_cats ) > 0 ) {
if ( has_term( $excluded_product_cats, 'product_cat', $item['product_id'] ) ) {
$included_cats = false;
}
}
if ( $included_products && $included_cats ) {
$coupon_codes[] = $coupon->get_code();
$discount = $item['subtotal'] - $item['total'];
if ( wc_prices_include_tax() ) {
$discount = $discount + $item['subtotal_tax'] - $item['total_tax'];
}
if ( isset( $item['quantity'] ) && $item['quantity'] > 0 ) {
$discount = $discount / $item['quantity'];
} else {
$discount = 0;
}
}
}
}
return [
'coupon_codes' => $coupon_codes,
'discount' => $discount,
];
}
/**
* Add-to-cart tracing on single product.
*
* @hook woocommerce_after_add_to_cart_button
*
* @return void
*/
public function single_product_add_to_cart_tracking(): void {
global $product;
$item_data = $this->get_item_data( $product );
echo '<input type="hidden" name="gtmkit_product_data' . '" value="' . esc_attr( json_encode( $item_data ) ) . '" />' . "\n"; // phpcs:ignore
}
/**
* Add-to-cart tracking on grouped product.
*
* @hook woocommerce_grouped_product_list_column_label
*
* @param string $label_value Product label.
* @param WC_Product $product The product.
*
* @return string The product label string.
*/
public function grouped_product_add_to_cart_tracking( string $label_value, WC_Product $product ): string {
$label_value .= $this->get_item_data_tag( $product, __( 'Grouped Product', 'gtm-kit' ), $this->grouped_product_position++ );
return $label_value;
}
/**
* Add-to-cart tracking on product blocks
*
* @hook woocommerce_blocks_product_grid_item_html.
*
* @param string $html Product grid item HTML.
* @param object $data Product data passed to the template.
* @param WC_Product $product Product object.
*
* @return string Updated product grid item HTML.
*/
public function product_block_add_to_cart_tracking( string $html, object $data, WC_Product $product ): string {
$item_data_tag = $this->get_item_data_tag( $product, '', 0 );
return preg_replace( '/<li[^>]+class="[^"]*wc-block-grid__product[^">]*"[^>]*>/i', '$0' . $item_data_tag, $html );
}
/**
* Generates a hidden <span> element that contains the item data.
*
* @param WC_Product $product Product object.
* @param string $item_list_name Name of the list associated with the event.
* @param int $index The index of the product in the product list. The first product should have the index no. 1.
*
* @return string A hidden <span> element that contains the item data.
*/
public function get_item_data_tag( WC_Product $product, string $item_list_name, int $index ): string {
if ( empty( $item_list_name ) ) {
$item_list_name = ( is_search() ) ? __( 'Search Results', 'gtm-kit' ) : __( 'General Product List', 'gtm-kit' );
}
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$posts_per_page = get_query_var( 'posts_per_page' );
if ( $posts_per_page < 1 ) {
$posts_per_page = 1;
}
$index = $index + ( $posts_per_page * ( $paged - 1 ) );
$item_data = $this->get_item_data(
$product,
[
'item_list_name' => $item_list_name,
'index' => $index,
],
'product_list'
);
return sprintf(
'<span class="gtmkit_product_data" style="display:none; visibility:hidden;" data-gtmkit_product_id="%s" data-gtmkit_product_data="%s"></span>',
esc_attr( (string) $product->get_id() ),
esc_attr( wp_json_encode( $item_data ) )
);
}
/**
* Add-to-cart tracking in product list loop
*
* @hook woocommerce_after_shop_loop_item.
*
* @return void
*/
public function product_list_loop_add_to_cart_tracking(): void {
global $product, $woocommerce_loop;
if ( ! empty( $woocommerce_loop['gtmkit_list_name'] ) ) {
$list_name = $woocommerce_loop['gtmkit_list_name'];
} else {
$list_name = __( 'General Product List', 'gtm-kit' );
}
echo wp_kses(
$this->get_item_data_tag(
$product,
$list_name,
( $woocommerce_loop['loop'] ) ?? 0
),
[
'span' => [
'class' => [],
'style' => [],
'data-gtmkit_product_id' => [],
'data-gtmkit_product_data' => [],
],
]
);
}
/**
* Set list name in WooCommerce loop
*
* @hook woocommerce_after_shop_loop_item.
*
* @return void
*/
public function set_list_name_in_woocommerce_loop(): void {
global $woocommerce_loop;
if ( ! empty( $woocommerce_loop['name'] ) ) {
$woocommerce_loop['gtmkit_list_name'] = ucwords( str_replace( '_', ' ', $woocommerce_loop['name'] ) );
} else {
$woocommerce_loop['gtmkit_list_name'] = __( 'General Product List', 'gtm-kit' );
}
}
/**
* Set list name in WooCommerce loop
*
* @hook woocommerce_after_shop_loop_item.
*
* @param mixed $columns The columns.
*
* @return mixed
*/
public function set_list_name_in_woocommerce_loop_filter( $columns ) {
global $woocommerce_loop;
$this->set_list_name_in_woocommerce_loop();
return $columns;
}
/**
* Set the list name on categories and tags
*
* @param mixed $value The product loop start.
*
* @return mixed
*/
public function set_list_name_on_category_and_tag( $value ) {
global $woocommerce_loop;
if ( isset( $woocommerce_loop['name'] ) && empty( $woocommerce_loop['name'] ) ) {
if ( is_product_category() ) {
$woocommerce_loop['gtmkit_list_name'] = __( 'Product Category', 'gtm-kit' );
} elseif ( is_product_tag() ) {
$woocommerce_loop['gtmkit_list_name'] = __( 'Product Tag', 'gtm-kit' );
}
}
return $value;
}
/**
* Add product data to cart item remove link
*
* @hook woocommerce_cart_item_remove_link.
*
* @param string $woocommerce_cart_item_remove_link The cart item remove link.
* @param string $cart_item_key The cart item key.
*
* @return string The updated cart item remove link containing product data.
*/
public function cart_item_remove_link( string $woocommerce_cart_item_remove_link, string $cart_item_key ): string {
$cart_item = WC()->cart->get_cart_item( $cart_item_key );
if ( ! $cart_item || ! $cart_item['quantity'] ) {
return $woocommerce_cart_item_remove_link;
}
$item_data = $this->get_item_data(
$cart_item['data'],
[
'quantity' => $cart_item['quantity'],
],
'remove_from_cart'
);
$link_html = new \WP_HTML_Tag_Processor( $woocommerce_cart_item_remove_link );
$link_html->next_tag();
$link_html->set_attribute( 'data-gtmkit_product_data', esc_attr( wp_json_encode( $item_data ) ) );
return $link_html->get_updated_html();
}
/**
* Prefix an item ID
*
* @param string $item_id The item ID.
*
* @return string
*/
public function prefix_item_id( string $item_id = '' ): string {
return $this->options->get( 'integrations', 'woocommerce_product_id_prefix' ) . $item_id;
}
/**
* Registers the actual data into each endpoint.
*/
public function extend_store(): void {
// Register into `cart/items`.
$this->extend->register_endpoint_data(
[
'endpoint' => ProductSchema::IDENTIFIER,
'namespace' => 'gtmkit',
'data_callback' => [ self::$instance, 'extend_product_data' ],
'schema_callback' => [ self::$instance, 'extend_product_schema' ],
'schema_type' => ARRAY_A,
]
);
$this->extend->register_endpoint_data(
[
'endpoint' => CartItemSchema::IDENTIFIER,
'namespace' => 'gtmkit',
'data_callback' => [ self::$instance, 'extend_cart_data' ],
'schema_callback' => [ self::$instance, 'extend_product_schema' ],
'schema_type' => ARRAY_A,
]
);
}
/**
* Register GTM data into products endpoint.
*
* @param WC_Product $product Current product data.
*
* @return array<string, mixed> $product Registered data or empty array if condition is not satisfied.
*/
public function extend_product_data( $product ): array {
return [
'item' => $this->get_item_data( $product ),
];
}
/**
* Register GTM data into products endpoint.
*
* @param array<string, mixed> $cart_item Cart item data.
*
* @return array<string, mixed> $product Registered data or empty array if condition is not satisfied.
*/
public function extend_cart_data( array $cart_item ): array {
return [
'item' => wp_json_encode( $this->get_item_data( $cart_item['data'] ) ),
];
}
/**
* Register subscription product schema into cart/items endpoint.
*
* @return array<string, mixed> Registered schema.
*/
public function extend_product_schema(): array {
return [
'gtmkit_data' => [
'description' => __( 'GTM Kit data.', 'gtm-kit' ),
'type' => [ 'string', 'null' ],
'readonly' => true,
],
];
}
/**
* Has WooCommerce blocks
*
* @param int|null $post_id The post ID.
*
* @return array<int, mixed>
*/
public function has_woocommerce_blocks( ?int $post_id ): array {
if ( null === $post_id ) {
return [];
}
$post_content = get_the_content( null, false, $post_id );
$woocommerce_blocks = [];
// This will return an array of blocks.
$blocks = parse_blocks( $post_content );
// Then you can loop over the array and check if any of the blocks are WooCommerce blocks.
foreach ( $blocks as $block ) {
if ( ! empty( $block['blockName'] ) && strpos( $block['blockName'], 'woocommerce/' ) !== false ) {
$woocommerce_blocks[] = str_replace( 'woocommerce/', '', $block['blockName'] );
}
}
return $woocommerce_blocks;
}
/**
* Get WooCommerce blocks
*
* @return array<int, mixed>
*/
public function get_woocommerce_blocks(): array {
return $this->has_woocommerce_blocks( get_the_ID() );
}
/**
* Include customer data
*
* @param array<string, mixed> $data_layer The datalayer content.
* @param mixed $order_value Order value.
*
* @return array<string, mixed>
*/
public function include_customer_data( array $data_layer, $order_value ): array {
if ( is_user_logged_in() ) {
try {
$wc_customer = new WC_Customer( WC()->customer->get_id() );
$order_count = $wc_customer->get_order_count();
$total_spent = $wc_customer->get_total_spent();
} catch ( Exception $e ) {
$wc_customer = WC()->customer;
$order_count = 1;
$total_spent = $order_value;
}
} else {
$wc_customer = WC()->customer;
$order_count = 1;
$total_spent = $order_value;
}
$data_layer['ecommerce']['customer']['id'] = $wc_customer->get_id();
$data_layer['ecommerce']['customer']['order_count'] = $order_count;
$data_layer['ecommerce']['customer']['total_spent'] = round( $total_spent, 2 );
$data_layer['ecommerce']['customer']['first_name'] = $wc_customer->get_first_name();
$data_layer['ecommerce']['customer']['last_name'] = $wc_customer->get_last_name();
$data_layer['ecommerce']['customer']['billing_first_name'] = $wc_customer->get_billing_first_name();
$data_layer['ecommerce']['customer']['billing_last_name'] = $wc_customer->get_billing_last_name();
$data_layer['ecommerce']['customer']['billing_company'] = $wc_customer->get_billing_company();
$data_layer['ecommerce']['customer']['billing_address_1'] = $wc_customer->get_billing_address_1();
$data_layer['ecommerce']['customer']['billing_address_2'] = $wc_customer->get_billing_address_2();
$data_layer['ecommerce']['customer']['billing_city'] = $wc_customer->get_billing_city();
$data_layer['ecommerce']['customer']['billing_postcode'] = $wc_customer->get_billing_postcode();
$data_layer['ecommerce']['customer']['billing_country'] = $wc_customer->get_billing_country();
$data_layer['ecommerce']['customer']['billing_state'] = $wc_customer->get_billing_state();
$data_layer['ecommerce']['customer']['billing_email'] = $wc_customer->get_billing_email();
$data_layer['ecommerce']['customer']['billing_email_hash'] = ( $wc_customer->get_billing_email() ) ? hash( 'sha256', $wc_customer->get_billing_email() ) : '';
$data_layer['ecommerce']['customer']['billing_phone'] = $wc_customer->get_billing_phone();
$data_layer['ecommerce']['customer']['shipping_firstName'] = $wc_customer->get_shipping_first_name();
$data_layer['ecommerce']['customer']['shipping_lastName'] = $wc_customer->get_shipping_last_name();
$data_layer['ecommerce']['customer']['shipping_company'] = $wc_customer->get_shipping_company();
$data_layer['ecommerce']['customer']['shipping_address_1'] = $wc_customer->get_shipping_address_1();
$data_layer['ecommerce']['customer']['shipping_address_2'] = $wc_customer->get_shipping_address_2();
$data_layer['ecommerce']['customer']['shipping_city'] = $wc_customer->get_shipping_city();
$data_layer['ecommerce']['customer']['shipping_postcode'] = $wc_customer->get_shipping_postcode();
$data_layer['ecommerce']['customer']['shipping_country'] = $wc_customer->get_shipping_country();
$data_layer['ecommerce']['customer']['shipping_state'] = $wc_customer->get_shipping_state();
$data_layer['user_data']['sha256_email_address'] = $this->util->normalize_and_hash_email_address( 'sha256', $wc_customer->get_billing_email() );
$data_layer['user_data']['sha256_phone_number'] = $this->util->normalize_and_hash( 'sha256', $wc_customer->get_billing_phone(), true );
$data_layer['user_data']['address']['sha256_first_name'] = $this->util->normalize_and_hash( 'sha256', $wc_customer->get_billing_first_name(), false );
$data_layer['user_data']['address']['sha256_last_name'] = $this->util->normalize_and_hash( 'sha256', $wc_customer->get_billing_last_name(), false );
$data_layer['user_data']['address']['street'] = $wc_customer->get_billing_address_1();
$data_layer['user_data']['address']['city'] = $wc_customer->get_billing_city();
$data_layer['user_data']['address']['region'] = $wc_customer->get_billing_state();
$data_layer['user_data']['address']['postal_code'] = $wc_customer->get_billing_postcode();
$data_layer['user_data']['address']['country'] = $wc_customer->get_billing_country();
return $data_layer;
}
/**
* Get order items
*
* @param WC_Order $order The order.
*
* @return array<int, mixed>
*/
private function get_order_items( WC_Order $order ): array {
$order_items = [];
$coupons = $order->get_coupon_codes();
$items = $order->get_items();
if ( $items ) {
foreach ( $items as $item ) {
if ( $item instanceof WC_Order_Item_Product ) {
$product = $item->get_product();
$inc_tax = ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) );
$product_price = round( $order->get_item_total( $item, $inc_tax ), 2 );
$additional_item_attributes = [
'quantity' => $item->get_quantity(),
'price' => $product_price,
];
$coupon_discount = $this->get_coupon_discount( $coupons, $item->get_data() );
if ( $coupon_discount['coupon_codes'] ) {
$additional_item_attributes['coupon'] = implode( '|', array_filter( $coupon_discount['coupon_codes'] ) );
}
if ( $coupon_discount['discount'] ) {
$additional_item_attributes['discount'] = round( (float) $coupon_discount['discount'], 2 );
}
$order_items[] = $this->get_item_data(
$product,
$additional_item_attributes,
'purchase'
);
}
}
}
return $order_items;
}
/**
* I the current page the custom order received page
*
* @param bool $is_order_received_page True when viewing the order received page.
*
* @return bool
*/
public function is_custom_order_received_page( bool $is_order_received_page ): bool {
// If WooCommerce already detected it, respect that.
if ( $is_order_received_page ) {
return true;
}
if ( is_admin() && ! wp_doing_ajax() ) {
return false;
}
$page_id = $this->options->get( 'integrations', 'woocommerce_custom_order_received_page' );
if ( ! empty( $page_id ) && is_page( $page_id ) ) {
return true;
}
return false;
}
}