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; } }