Files
wc-composable-product/includes/Cart_Handler.php
magdev 1edb0be3d9 Initial implementation of WooCommerce Composable Products plugin
Implemented custom WooCommerce product type allowing customers to build their own
product bundles by selecting from predefined sets of products.

Features:
- Custom "Composable Product" type with admin interface
- Product selection by category, tag, or SKU
- Configurable selection limits (global and per-product)
- Dual pricing modes: fixed price or sum of selected products
- Modern responsive frontend with Twig templates
- AJAX add-to-cart functionality
- Full internationalization support (.pot file)
- WooCommerce settings integration
- Comprehensive documentation

Technical implementation:
- PHP 8.3+ with PSR-4 autoloading
- Twig 3.0 templating engine via Composer
- Vanilla JavaScript with jQuery for frontend interactions
- WordPress and WooCommerce hooks for seamless integration
- Security: input sanitization, validation, and output escaping
- Translation-ready with text domain 'wc-composable-product'

Documentation:
- README.md: Project overview and features
- INSTALL.md: Installation and usage guide
- IMPLEMENTATION.md: Technical architecture
- CHANGELOG.md: Version history

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 00:38:29 +01:00

182 lines
5.8 KiB
PHP

<?php
/**
* Cart Handler
*
* @package WC_Composable_Product
*/
namespace WC_Composable_Product;
defined('ABSPATH') || exit;
/**
* Cart Handler Class
*
* Handles adding composable products to cart and calculating prices
*/
class Cart_Handler {
/**
* Constructor
*/
public function __construct() {
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);
}
/**
* 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;
}
}
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;
}
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);
}
}
}
}
}