stock_manager = new Stock_Manager(); add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3); add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 2); add_filter('woocommerce_get_cart_item_from_session', [$this, 'get_cart_item_from_session'], 10, 2); add_filter('woocommerce_get_item_data', [$this, 'display_cart_item_data'], 10, 2); add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']); add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25); add_action('woocommerce_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3); add_filter('woocommerce_is_purchasable', [$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') { Product_Selector::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 if (!isset($_POST['composable_products']) || empty($_POST['composable_products'])) { wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error'); return false; } $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)) { 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 ($stock_validation !== true) { 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; } if (isset($_POST['composable_products']) && !empty($_POST['composable_products'])) { $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(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 = []; 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[] = [ '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 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']) && !isset($cart_item['composable_price_calculated'])) { $product = $cart_item['data']; $price = $product->calculate_composed_price($cart_item['composable_products']); $cart_item['data']->set_price($price); // Mark as calculated to prevent re-calculation by other plugins $cart->cart_contents[$cart_item_key]['composable_price_calculated'] = true; } } } $already_calculated = true; } }