Files
wc-composable-product/includes/CartHandler.php
magdev dea1b055b2
All checks were successful
Create Release Package / PHP Lint (push) Successful in 48s
Create Release Package / PHP CodeSniffer (push) Successful in 52s
Create Release Package / PHP Unit (push) Successful in 53s
Create Release Package / build-release (push) Successful in 59s
Upgrade to PHPUnit 10, add PHPCS with WPCS compliance, add phpcs CI job
- Upgrade PHPUnit 9.6 → 10, update phpunit.xml.dist schema
- Add PHPCS 3.13 with WordPress-Extra + PHPCompatibilityWP standards
- PHPCBF auto-fix + manual fixes for full WPCS compliance
- Add phpcs job to release workflow (parallel with lint)
- Pin composer platform to PHP 8.3 to prevent incompatible dep locks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:25:02 +01:00

229 lines
6.9 KiB
PHP

<?php
/**
* Cart Handler
*
* @package Magdev\WcComposableProduct
*/
namespace Magdev\WcComposableProduct;
defined( 'ABSPATH' ) || exit;
/**
* Cart Handler Class
*
* Handles adding composable products to cart and calculating prices
*/
class CartHandler {
/**
* 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;
}
}