2025-12-31 00:38:29 +01:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Cart Handler
|
|
|
|
|
*
|
2026-03-01 12:35:02 +01:00
|
|
|
* @package Magdev\WcComposableProduct
|
2025-12-31 00:38:29 +01:00
|
|
|
*/
|
|
|
|
|
|
2026-03-01 12:35:02 +01:00
|
|
|
namespace Magdev\WcComposableProduct;
|
2025-12-31 00:38:29 +01:00
|
|
|
|
2026-03-01 13:25:02 +01:00
|
|
|
defined( 'ABSPATH' ) || exit;
|
2025-12-31 00:38:29 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cart Handler Class
|
|
|
|
|
*
|
|
|
|
|
* Handles adding composable products to cart and calculating prices
|
|
|
|
|
*/
|
2026-03-01 12:35:02 +01:00
|
|
|
class CartHandler {
|
2026-03-01 13:25:02 +01:00
|
|
|
/**
|
|
|
|
|
* Stock manager instance
|
|
|
|
|
*
|
|
|
|
|
* @var StockManager
|
|
|
|
|
*/
|
|
|
|
|
private $stock_manager;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor
|
|
|
|
|
*/
|
|
|
|
|
public function __construct() {
|
|
|
|
|
$this->stock_manager = new StockManager();
|
|
|
|
|
|
|
|
|
|
add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'validate_add_to_cart' ), 10, 3 );
|
|
|
|
|
add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_cart_item_data' ), 10, 2 );
|
|
|
|
|
add_filter( 'woocommerce_get_cart_item_from_session', array( $this, 'get_cart_item_from_session' ), 10, 2 );
|
|
|
|
|
add_filter( 'woocommerce_get_item_data', array( $this, 'display_cart_item_data' ), 10, 2 );
|
|
|
|
|
add_action( 'woocommerce_before_calculate_totals', array( $this, 'calculate_cart_item_price' ) );
|
|
|
|
|
add_action( 'woocommerce_single_product_summary', array( $this, 'render_product_selector' ), 25 );
|
|
|
|
|
add_action( 'woocommerce_checkout_create_order_line_item', array( $this->stock_manager, 'store_selected_products_in_order' ), 10, 3 );
|
|
|
|
|
add_filter( 'woocommerce_is_purchasable', array( $this, 'hide_default_add_to_cart' ), 10, 2 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hide default WooCommerce add to cart button for composable products
|
|
|
|
|
*
|
|
|
|
|
* @param bool $is_purchasable Is purchasable status
|
|
|
|
|
* @param \WC_Product $product Product object
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function hide_default_add_to_cart( $is_purchasable, $product ) {
|
|
|
|
|
if ( $product && $product->get_type() === 'composable' ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return $is_purchasable;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render product selector on product page
|
|
|
|
|
*/
|
|
|
|
|
public function render_product_selector() {
|
|
|
|
|
global $product;
|
|
|
|
|
|
|
|
|
|
if ( $product && $product->get_type() === 'composable' ) {
|
|
|
|
|
ProductSelector::render( $product );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate add to cart
|
|
|
|
|
*
|
|
|
|
|
* @param bool $passed Validation status
|
|
|
|
|
* @param int $product_id Product ID
|
|
|
|
|
* @param int $quantity Quantity
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function validate_add_to_cart( $passed, $product_id, $quantity ) {
|
|
|
|
|
$product = wc_get_product( $product_id );
|
|
|
|
|
|
|
|
|
|
if ( ! $product || $product->get_type() !== 'composable' ) {
|
|
|
|
|
return $passed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if selected products are provided.
|
|
|
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
|
|
|
|
if ( ! isset( $_POST['composable_products'] ) || empty( $_POST['composable_products'] ) ) {
|
|
|
|
|
wc_add_notice( __( 'Please select at least one product.', 'wc-composable-product' ), 'error' );
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
|
|
|
|
$selected_products = array_map( 'absint', $_POST['composable_products'] );
|
|
|
|
|
$selection_limit = $product->get_selection_limit();
|
|
|
|
|
|
|
|
|
|
// Validate selection limit
|
|
|
|
|
if ( count( $selected_products ) > $selection_limit ) {
|
|
|
|
|
/* translators: %d: selection limit */
|
|
|
|
|
wc_add_notice( sprintf( __( 'You can select a maximum of %d products.', 'wc-composable-product' ), $selection_limit ), 'error' );
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( count( $selected_products ) === 0 ) {
|
|
|
|
|
wc_add_notice( __( 'Please select at least one product.', 'wc-composable-product' ), 'error' );
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate that selected products are valid
|
|
|
|
|
$available_products = $product->get_available_products();
|
|
|
|
|
$available_ids = array_map(
|
|
|
|
|
function ( $p ) {
|
|
|
|
|
return $p->get_id();
|
|
|
|
|
},
|
|
|
|
|
$available_products
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
foreach ( $selected_products as $selected_id ) {
|
|
|
|
|
if ( ! in_array( $selected_id, $available_ids, true ) ) {
|
|
|
|
|
wc_add_notice( __( 'One or more selected products are not available.', 'wc-composable-product' ), 'error' );
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate stock availability
|
|
|
|
|
$stock_validation = $this->stock_manager->validate_stock_availability( $selected_products, $quantity );
|
|
|
|
|
if ( true !== $stock_validation ) {
|
|
|
|
|
wc_add_notice( $stock_validation, 'error' );
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $passed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add cart item data
|
|
|
|
|
*
|
|
|
|
|
* @param array $cart_item_data Cart item data
|
|
|
|
|
* @param int $product_id Product ID
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function add_cart_item_data( $cart_item_data, $product_id ) {
|
|
|
|
|
$product = wc_get_product( $product_id );
|
|
|
|
|
|
|
|
|
|
if ( ! $product || $product->get_type() !== 'composable' ) {
|
|
|
|
|
return $cart_item_data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
|
|
|
|
if ( isset( $_POST['composable_products'] ) && ! empty( $_POST['composable_products'] ) ) {
|
|
|
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
|
|
|
|
$selected_products = array_map( 'absint', $_POST['composable_products'] );
|
|
|
|
|
$cart_item_data['composable_products'] = $selected_products;
|
|
|
|
|
|
|
|
|
|
// Make cart item unique.
|
|
|
|
|
$cart_item_data['unique_key'] = md5( wp_json_encode( $selected_products ) . time() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $cart_item_data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get cart item from session
|
|
|
|
|
*
|
|
|
|
|
* @param array $cart_item Cart item
|
|
|
|
|
* @param array $values Values from session
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function get_cart_item_from_session( $cart_item, $values ) {
|
|
|
|
|
if ( isset( $values['composable_products'] ) ) {
|
|
|
|
|
$cart_item['composable_products'] = $values['composable_products'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $cart_item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Display cart item data
|
|
|
|
|
*
|
|
|
|
|
* @param array $item_data Item data
|
|
|
|
|
* @param array $cart_item Cart item
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function display_cart_item_data( $item_data, $cart_item ) {
|
|
|
|
|
if ( isset( $cart_item['composable_products'] ) && ! empty( $cart_item['composable_products'] ) ) {
|
|
|
|
|
$product_names = array();
|
|
|
|
|
foreach ( $cart_item['composable_products'] as $product_id ) {
|
|
|
|
|
$product = wc_get_product( $product_id );
|
|
|
|
|
if ( $product ) {
|
|
|
|
|
$product_names[] = $product->get_name();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( ! empty( $product_names ) ) {
|
|
|
|
|
$item_data[] = array(
|
|
|
|
|
'key' => __( 'Selected Products', 'wc-composable-product' ),
|
|
|
|
|
'value' => implode( ', ', $product_names ),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $item_data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate cart item price
|
|
|
|
|
*
|
|
|
|
|
* @param \WC_Cart $cart Cart object
|
|
|
|
|
*/
|
|
|
|
|
public function calculate_cart_item_price( $cart ) {
|
|
|
|
|
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use static flag to prevent multiple executions within the same request
|
|
|
|
|
static $already_calculated = false;
|
|
|
|
|
if ( $already_calculated ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
|
|
|
|
|
if ( isset( $cart_item['data'] ) && $cart_item['data']->get_type() === 'composable' ) {
|
|
|
|
|
if ( isset( $cart_item['composable_products'] ) ) {
|
|
|
|
|
$product = $cart_item['data'];
|
|
|
|
|
$price = $product->calculate_composed_price( $cart_item['composable_products'] );
|
|
|
|
|
$cart_item['data']->set_price( $price );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$already_calculated = true;
|
|
|
|
|
}
|
2025-12-31 00:38:29 +01:00
|
|
|
}
|