You've already forked wc-composable-product
Upgrade to PHPUnit 10, add PHPCS with WPCS compliance, add phpcs CI job
All checks were successful
All checks were successful
- 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>
This commit is contained in:
@@ -7,211 +7,239 @@
|
||||
|
||||
namespace Magdev\WcComposableProduct\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Product Data Tab Class
|
||||
*/
|
||||
class ProductData {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter('woocommerce_product_data_tabs', [$this, 'add_product_data_tab']);
|
||||
add_action('woocommerce_product_data_panels', [$this, 'add_product_data_panel']);
|
||||
add_action('woocommerce_process_product_meta_composable', [$this, 'save_product_data']);
|
||||
add_action('woocommerce_product_options_general_product_data', [$this, 'add_general_fields']);
|
||||
}
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_data_tab' ) );
|
||||
add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_data_panel' ) );
|
||||
add_action( 'woocommerce_process_product_meta_composable', array( $this, 'save_product_data' ) );
|
||||
add_action( 'woocommerce_product_options_general_product_data', array( $this, 'add_general_fields' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add composable products tab
|
||||
*
|
||||
* @param array $tabs Product data tabs
|
||||
* @return array
|
||||
*/
|
||||
public function add_product_data_tab($tabs) {
|
||||
$tabs['composable'] = [
|
||||
'label' => __('Composable Options', 'wc-composable-product'),
|
||||
'target' => 'composable_product_data',
|
||||
'class' => ['show_if_composable'],
|
||||
'priority' => 21,
|
||||
];
|
||||
return $tabs;
|
||||
}
|
||||
/**
|
||||
* Add composable products tab
|
||||
*
|
||||
* @param array $tabs Product data tabs
|
||||
* @return array
|
||||
*/
|
||||
public function add_product_data_tab( $tabs ) {
|
||||
$tabs['composable'] = array(
|
||||
'label' => __( 'Composable Options', 'wc-composable-product' ),
|
||||
'target' => 'composable_product_data',
|
||||
'class' => array( 'show_if_composable' ),
|
||||
'priority' => 21,
|
||||
);
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add fields to general tab
|
||||
*/
|
||||
public function add_general_fields() {
|
||||
global $product_object;
|
||||
/**
|
||||
* Add fields to general tab
|
||||
*/
|
||||
public function add_general_fields() {
|
||||
global $product_object;
|
||||
|
||||
if ($product_object && $product_object->get_type() === 'composable') {
|
||||
echo '<div class="options_group show_if_composable">';
|
||||
if ( $product_object && $product_object->get_type() === 'composable' ) {
|
||||
echo '<div class="options_group show_if_composable">';
|
||||
|
||||
woocommerce_wp_text_input([
|
||||
'id' => '_composable_selection_limit',
|
||||
'label' => __('Selection Limit', 'wc-composable-product'),
|
||||
'description' => __('Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'type' => 'number',
|
||||
'custom_attributes' => [
|
||||
'min' => '1',
|
||||
'step' => '1',
|
||||
],
|
||||
]);
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '_composable_selection_limit',
|
||||
'label' => __( 'Selection Limit', 'wc-composable-product' ),
|
||||
'description' => __( 'Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product' ),
|
||||
'desc_tip' => true,
|
||||
'type' => 'number',
|
||||
'custom_attributes' => array(
|
||||
'min' => '1',
|
||||
'step' => '1',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
woocommerce_wp_select([
|
||||
'id' => '_composable_pricing_mode',
|
||||
'label' => __('Pricing Mode', 'wc-composable-product'),
|
||||
'description' => __('How to calculate the price.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'options' => [
|
||||
'' => __('Use global default', 'wc-composable-product'),
|
||||
'sum' => __('Sum of selected products', 'wc-composable-product'),
|
||||
'fixed' => __('Fixed price', 'wc-composable-product'),
|
||||
],
|
||||
]);
|
||||
woocommerce_wp_select(
|
||||
array(
|
||||
'id' => '_composable_pricing_mode',
|
||||
'label' => __( 'Pricing Mode', 'wc-composable-product' ),
|
||||
'description' => __( 'How to calculate the price.', 'wc-composable-product' ),
|
||||
'desc_tip' => true,
|
||||
'options' => array(
|
||||
'' => __( 'Use global default', 'wc-composable-product' ),
|
||||
'sum' => __( 'Sum of selected products', 'wc-composable-product' ),
|
||||
'fixed' => __( 'Fixed price', 'wc-composable-product' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
woocommerce_wp_text_input([
|
||||
'id' => '_regular_price',
|
||||
'label' => __('Fixed Price', 'wc-composable-product') . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||
'description' => __('Enter the fixed price for this composable product.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'type' => 'text',
|
||||
'data_type' => 'price',
|
||||
'wrapper_class' => 'composable_fixed_price_field',
|
||||
]);
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '_regular_price',
|
||||
'label' => __( 'Fixed Price', 'wc-composable-product' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||
'description' => __( 'Enter the fixed price for this composable product.', 'wc-composable-product' ),
|
||||
'desc_tip' => true,
|
||||
'type' => 'text',
|
||||
'data_type' => 'price',
|
||||
'wrapper_class' => 'composable_fixed_price_field',
|
||||
)
|
||||
);
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product data panel
|
||||
*/
|
||||
public function add_product_data_panel() {
|
||||
global $post;
|
||||
?>
|
||||
<div id="composable_product_data" class="panel woocommerce_options_panel hidden">
|
||||
<div class="options_group">
|
||||
<?php
|
||||
woocommerce_wp_select([
|
||||
'id' => '_composable_include_unpublished',
|
||||
'label' => __('Include Non-Public Products', 'wc-composable-product'),
|
||||
'description' => __('Allow draft and private products in the selection. Useful when products should only be sold as part of a composition.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'options' => [
|
||||
'' => __('Use global default', 'wc-composable-product'),
|
||||
'yes' => __('Yes', 'wc-composable-product'),
|
||||
'no' => __('No', 'wc-composable-product'),
|
||||
],
|
||||
'value' => get_post_meta($post->ID, '_composable_include_unpublished', true) ?: '',
|
||||
]);
|
||||
/**
|
||||
* Add product data panel
|
||||
*/
|
||||
public function add_product_data_panel() {
|
||||
global $post;
|
||||
?>
|
||||
<div id="composable_product_data" class="panel woocommerce_options_panel hidden">
|
||||
<div class="options_group">
|
||||
<?php
|
||||
woocommerce_wp_select(
|
||||
array(
|
||||
'id' => '_composable_include_unpublished',
|
||||
'label' => __( 'Include Non-Public Products', 'wc-composable-product' ),
|
||||
'description' => __( 'Allow draft and private products in the selection. Useful when products should only be sold as part of a composition.', 'wc-composable-product' ),
|
||||
'desc_tip' => true,
|
||||
'options' => array(
|
||||
'' => __( 'Use global default', 'wc-composable-product' ),
|
||||
'yes' => __( 'Yes', 'wc-composable-product' ),
|
||||
'no' => __( 'No', 'wc-composable-product' ),
|
||||
),
|
||||
'value' => get_post_meta( $post->ID, '_composable_include_unpublished', true ) ? get_post_meta( $post->ID, '_composable_include_unpublished', true ) : '',
|
||||
)
|
||||
);
|
||||
|
||||
woocommerce_wp_select([
|
||||
'id' => '_composable_criteria_type',
|
||||
'label' => __('Selection Criteria', 'wc-composable-product'),
|
||||
'description' => __('How to select available products.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'options' => [
|
||||
'category' => __('By Category', 'wc-composable-product'),
|
||||
'tag' => __('By Tag', 'wc-composable-product'),
|
||||
'sku' => __('By SKU', 'wc-composable-product'),
|
||||
],
|
||||
'value' => get_post_meta($post->ID, '_composable_criteria_type', true) ?: 'category',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
woocommerce_wp_select(
|
||||
array(
|
||||
'id' => '_composable_criteria_type',
|
||||
'label' => __( 'Selection Criteria', 'wc-composable-product' ),
|
||||
'description' => __( 'How to select available products.', 'wc-composable-product' ),
|
||||
'desc_tip' => true,
|
||||
'options' => array(
|
||||
'category' => __( 'By Category', 'wc-composable-product' ),
|
||||
'tag' => __( 'By Tag', 'wc-composable-product' ),
|
||||
'sku' => __( 'By SKU', 'wc-composable-product' ),
|
||||
),
|
||||
'value' => get_post_meta( $post->ID, '_composable_criteria_type', true ) ? get_post_meta( $post->ID, '_composable_criteria_type', true ) : 'category',
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_category">
|
||||
<p class="form-field">
|
||||
<label for="_composable_categories"><?php esc_html_e('Select Categories', 'wc-composable-product'); ?></label>
|
||||
<select id="_composable_categories" name="_composable_categories[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||
<?php
|
||||
$selected_categories = get_post_meta($post->ID, '_composable_categories', true) ?: [];
|
||||
$categories = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => false]);
|
||||
foreach ($categories as $category) {
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr($category->term_id),
|
||||
selected(in_array($category->term_id, (array) $selected_categories), true, false),
|
||||
esc_html($category->name)
|
||||
);
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<span class="description"><?php esc_html_e('Select product categories to include.', 'wc-composable-product'); ?></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_category">
|
||||
<p class="form-field">
|
||||
<label for="_composable_categories"><?php esc_html_e( 'Select Categories', 'wc-composable-product' ); ?></label>
|
||||
<select id="_composable_categories" name="_composable_categories[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||
<?php
|
||||
$selected_categories = get_post_meta( $post->ID, '_composable_categories', true );
|
||||
$selected_categories = $selected_categories ? $selected_categories : array();
|
||||
$categories = get_terms(
|
||||
array(
|
||||
'taxonomy' => 'product_cat',
|
||||
'hide_empty' => false,
|
||||
)
|
||||
);
|
||||
foreach ( $categories as $category ) {
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr( $category->term_id ),
|
||||
selected( in_array( $category->term_id, (array) $selected_categories, true ), true, false ),
|
||||
esc_html( $category->name )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<span class="description"><?php esc_html_e( 'Select product categories to include.', 'wc-composable-product' ); ?></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_tag" style="display: none;">
|
||||
<p class="form-field">
|
||||
<label for="_composable_tags"><?php esc_html_e('Select Tags', 'wc-composable-product'); ?></label>
|
||||
<select id="_composable_tags" name="_composable_tags[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||
<?php
|
||||
$selected_tags = get_post_meta($post->ID, '_composable_tags', true) ?: [];
|
||||
$tags = get_terms(['taxonomy' => 'product_tag', 'hide_empty' => false]);
|
||||
foreach ($tags as $tag) {
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr($tag->term_id),
|
||||
selected(in_array($tag->term_id, (array) $selected_tags), true, false),
|
||||
esc_html($tag->name)
|
||||
);
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<span class="description"><?php esc_html_e('Select product tags to include.', 'wc-composable-product'); ?></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_tag" style="display: none;">
|
||||
<p class="form-field">
|
||||
<label for="_composable_tags"><?php esc_html_e( 'Select Tags', 'wc-composable-product' ); ?></label>
|
||||
<select id="_composable_tags" name="_composable_tags[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||
<?php
|
||||
$selected_tags = get_post_meta( $post->ID, '_composable_tags', true );
|
||||
$selected_tags = $selected_tags ? $selected_tags : array();
|
||||
$tags = get_terms(
|
||||
array(
|
||||
'taxonomy' => 'product_tag',
|
||||
'hide_empty' => false,
|
||||
)
|
||||
);
|
||||
foreach ( $tags as $tag ) {
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr( $tag->term_id ),
|
||||
selected( in_array( $tag->term_id, (array) $selected_tags, true ), true, false ),
|
||||
esc_html( $tag->name )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<span class="description"><?php esc_html_e( 'Select product tags to include.', 'wc-composable-product' ); ?></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_sku" style="display: none;">
|
||||
<?php
|
||||
woocommerce_wp_textarea_input([
|
||||
'id' => '_composable_skus',
|
||||
'label' => __('Product SKUs', 'wc-composable-product'),
|
||||
'description' => __('Enter product SKUs separated by commas.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __('SKU-1, SKU-2, SKU-3', 'wc-composable-product'),
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_sku" style="display: none;">
|
||||
<?php
|
||||
woocommerce_wp_textarea_input(
|
||||
array(
|
||||
'id' => '_composable_skus',
|
||||
'label' => __( 'Product SKUs', 'wc-composable-product' ),
|
||||
'description' => __( 'Enter product SKUs separated by commas.', 'wc-composable-product' ),
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'SKU-1, SKU-2, SKU-3', 'wc-composable-product' ),
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product data
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
*/
|
||||
public function save_product_data($post_id) {
|
||||
// Save selection limit
|
||||
$selection_limit = isset($_POST['_composable_selection_limit']) ? absint($_POST['_composable_selection_limit']) : '';
|
||||
update_post_meta($post_id, '_composable_selection_limit', $selection_limit);
|
||||
/**
|
||||
* Save product data
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
*/
|
||||
public function save_product_data( $post_id ) {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce in woocommerce_process_product_meta.
|
||||
|
||||
// Save pricing mode
|
||||
$pricing_mode = isset($_POST['_composable_pricing_mode']) ? sanitize_text_field($_POST['_composable_pricing_mode']) : '';
|
||||
update_post_meta($post_id, '_composable_pricing_mode', $pricing_mode);
|
||||
// Save selection limit.
|
||||
$selection_limit = isset( $_POST['_composable_selection_limit'] ) ? absint( $_POST['_composable_selection_limit'] ) : '';
|
||||
update_post_meta( $post_id, '_composable_selection_limit', $selection_limit );
|
||||
|
||||
// Save include unpublished
|
||||
$include_unpublished = isset($_POST['_composable_include_unpublished']) ? sanitize_text_field($_POST['_composable_include_unpublished']) : '';
|
||||
update_post_meta($post_id, '_composable_include_unpublished', $include_unpublished);
|
||||
// Save pricing mode.
|
||||
$pricing_mode = isset( $_POST['_composable_pricing_mode'] ) ? sanitize_text_field( $_POST['_composable_pricing_mode'] ) : '';
|
||||
update_post_meta( $post_id, '_composable_pricing_mode', $pricing_mode );
|
||||
|
||||
// Save criteria type
|
||||
$criteria_type = isset($_POST['_composable_criteria_type']) ? sanitize_text_field($_POST['_composable_criteria_type']) : 'category';
|
||||
update_post_meta($post_id, '_composable_criteria_type', $criteria_type);
|
||||
// Save include unpublished.
|
||||
$include_unpublished = isset( $_POST['_composable_include_unpublished'] ) ? sanitize_text_field( $_POST['_composable_include_unpublished'] ) : '';
|
||||
update_post_meta( $post_id, '_composable_include_unpublished', $include_unpublished );
|
||||
|
||||
// Save categories
|
||||
$categories = isset($_POST['_composable_categories']) ? array_map('absint', $_POST['_composable_categories']) : [];
|
||||
update_post_meta($post_id, '_composable_categories', $categories);
|
||||
// Save criteria type.
|
||||
$criteria_type = isset( $_POST['_composable_criteria_type'] ) ? sanitize_text_field( $_POST['_composable_criteria_type'] ) : 'category';
|
||||
update_post_meta( $post_id, '_composable_criteria_type', $criteria_type );
|
||||
|
||||
// Save tags
|
||||
$tags = isset($_POST['_composable_tags']) ? array_map('absint', $_POST['_composable_tags']) : [];
|
||||
update_post_meta($post_id, '_composable_tags', $tags);
|
||||
// Save categories.
|
||||
$categories = isset( $_POST['_composable_categories'] ) ? array_map( 'absint', $_POST['_composable_categories'] ) : array();
|
||||
update_post_meta( $post_id, '_composable_categories', $categories );
|
||||
|
||||
// Save SKUs
|
||||
$skus = isset($_POST['_composable_skus']) ? sanitize_textarea_field($_POST['_composable_skus']) : '';
|
||||
update_post_meta($post_id, '_composable_skus', $skus);
|
||||
}
|
||||
// Save tags.
|
||||
$tags = isset( $_POST['_composable_tags'] ) ? array_map( 'absint', $_POST['_composable_tags'] ) : array();
|
||||
update_post_meta( $post_id, '_composable_tags', $tags );
|
||||
|
||||
// Save SKUs.
|
||||
$skus = isset( $_POST['_composable_skus'] ) ? sanitize_textarea_field( $_POST['_composable_skus'] ) : '';
|
||||
update_post_meta( $post_id, '_composable_skus', $skus );
|
||||
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,109 +7,109 @@
|
||||
|
||||
namespace Magdev\WcComposableProduct\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Settings class
|
||||
*/
|
||||
class Settings extends \WC_Settings_Page {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->id = 'composable_products';
|
||||
$this->label = __('Composable Products', 'wc-composable-product');
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->id = 'composable_products';
|
||||
$this->label = __( 'Composable Products', 'wc-composable-product' );
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_settings() {
|
||||
$settings = [
|
||||
[
|
||||
'title' => __('Composable Products Settings', 'wc-composable-product'),
|
||||
'type' => 'title',
|
||||
'desc' => __('Configure default settings for composable products.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_settings',
|
||||
],
|
||||
[
|
||||
'title' => __('Default Selection Limit', 'wc-composable-product'),
|
||||
'desc' => __('Default number of items customers can select.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_default_limit',
|
||||
'type' => 'number',
|
||||
'default' => '5',
|
||||
'custom_attributes' => [
|
||||
'min' => '1',
|
||||
'step' => '1',
|
||||
],
|
||||
'desc_tip' => true,
|
||||
],
|
||||
[
|
||||
'title' => __('Default Pricing Mode', 'wc-composable-product'),
|
||||
'desc' => __('How to calculate the price of composable products.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_default_pricing',
|
||||
'type' => 'select',
|
||||
'default' => 'sum',
|
||||
'options' => [
|
||||
'sum' => __('Sum of selected products', 'wc-composable-product'),
|
||||
'fixed' => __('Fixed price', 'wc-composable-product'),
|
||||
],
|
||||
'desc_tip' => true,
|
||||
],
|
||||
[
|
||||
'title' => __('Include Non-Public Products', 'wc-composable-product'),
|
||||
'desc' => __('Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_include_unpublished',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'no',
|
||||
],
|
||||
[
|
||||
'title' => __('Show Product Images', 'wc-composable-product'),
|
||||
'desc' => __('Display product images in the selection interface.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_show_images',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
],
|
||||
[
|
||||
'title' => __('Show Product Prices', 'wc-composable-product'),
|
||||
'desc' => __('Display individual product prices in the selection interface.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_show_prices',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
],
|
||||
[
|
||||
'title' => __('Show Total Price', 'wc-composable-product'),
|
||||
'desc' => __('Display the total price as customers make selections.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_show_total',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
],
|
||||
[
|
||||
'type' => 'sectionend',
|
||||
'id' => 'wc_composable_settings',
|
||||
],
|
||||
];
|
||||
/**
|
||||
* Get settings array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_settings() {
|
||||
$settings = array(
|
||||
array(
|
||||
'title' => __( 'Composable Products Settings', 'wc-composable-product' ),
|
||||
'type' => 'title',
|
||||
'desc' => __( 'Configure default settings for composable products.', 'wc-composable-product' ),
|
||||
'id' => 'wc_composable_settings',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Default Selection Limit', 'wc-composable-product' ),
|
||||
'desc' => __( 'Default number of items customers can select.', 'wc-composable-product' ),
|
||||
'id' => 'wc_composable_default_limit',
|
||||
'type' => 'number',
|
||||
'default' => '5',
|
||||
'custom_attributes' => array(
|
||||
'min' => '1',
|
||||
'step' => '1',
|
||||
),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Default Pricing Mode', 'wc-composable-product' ),
|
||||
'desc' => __( 'How to calculate the price of composable products.', 'wc-composable-product' ),
|
||||
'id' => 'wc_composable_default_pricing',
|
||||
'type' => 'select',
|
||||
'default' => 'sum',
|
||||
'options' => array(
|
||||
'sum' => __( 'Sum of selected products', 'wc-composable-product' ),
|
||||
'fixed' => __( 'Fixed price', 'wc-composable-product' ),
|
||||
),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Include Non-Public Products', 'wc-composable-product' ),
|
||||
'desc' => __( 'Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually.', 'wc-composable-product' ),
|
||||
'id' => 'wc_composable_include_unpublished',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'no',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Show Product Images', 'wc-composable-product' ),
|
||||
'desc' => __( 'Display product images in the selection interface.', 'wc-composable-product' ),
|
||||
'id' => 'wc_composable_show_images',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Show Product Prices', 'wc-composable-product' ),
|
||||
'desc' => __( 'Display individual product prices in the selection interface.', 'wc-composable-product' ),
|
||||
'id' => 'wc_composable_show_prices',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Show Total Price', 'wc-composable-product' ),
|
||||
'desc' => __( 'Display the total price as customers make selections.', 'wc-composable-product' ),
|
||||
'id' => 'wc_composable_show_total',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
),
|
||||
array(
|
||||
'type' => 'sectionend',
|
||||
'id' => 'wc_composable_settings',
|
||||
),
|
||||
);
|
||||
|
||||
return apply_filters('wc_composable_settings', $settings);
|
||||
}
|
||||
return apply_filters( 'wc_composable_settings', $settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the settings
|
||||
*/
|
||||
public function output() {
|
||||
$settings = $this->get_settings();
|
||||
\WC_Admin_Settings::output_fields($settings);
|
||||
}
|
||||
/**
|
||||
* Output the settings
|
||||
*/
|
||||
public function output() {
|
||||
$settings = $this->get_settings();
|
||||
\WC_Admin_Settings::output_fields( $settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings
|
||||
*/
|
||||
public function save() {
|
||||
$settings = $this->get_settings();
|
||||
\WC_Admin_Settings::save_fields($settings);
|
||||
}
|
||||
/**
|
||||
* Save settings
|
||||
*/
|
||||
public function save() {
|
||||
$settings = $this->get_settings();
|
||||
\WC_Admin_Settings::save_fields( $settings );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Cart Handler Class
|
||||
@@ -15,207 +15,214 @@ defined('ABSPATH') || exit;
|
||||
* Handles adding composable products to cart and calculating prices
|
||||
*/
|
||||
class CartHandler {
|
||||
/**
|
||||
* Stock manager instance
|
||||
*
|
||||
* @var StockManager
|
||||
*/
|
||||
private $stock_manager;
|
||||
/**
|
||||
* Stock manager instance
|
||||
*
|
||||
* @var StockManager
|
||||
*/
|
||||
private $stock_manager;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->stock_manager = new StockManager();
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->stock_manager = new StockManager();
|
||||
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* Render product selector on product page
|
||||
*/
|
||||
public function render_product_selector() {
|
||||
global $product;
|
||||
|
||||
if ($product && $product->get_type() === 'composable') {
|
||||
ProductSelector::render($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);
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
$selected_products = array_map('absint', $_POST['composable_products']);
|
||||
$selection_limit = $product->get_selection_limit();
|
||||
// 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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
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 ($stock_validation !== true) {
|
||||
wc_add_notice($stock_validation, '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;
|
||||
}
|
||||
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);
|
||||
/**
|
||||
* 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 ( ! $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;
|
||||
// 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(json_encode($selected_products) . time());
|
||||
}
|
||||
// Make cart item unique.
|
||||
$cart_item_data['unique_key'] = md5( wp_json_encode( $selected_products ) . time() );
|
||||
}
|
||||
|
||||
return $cart_item_data;
|
||||
}
|
||||
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'];
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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[] = [
|
||||
'key' => __('Selected Products', 'wc-composable-product'),
|
||||
'value' => implode(', ', $product_names),
|
||||
];
|
||||
}
|
||||
}
|
||||
if ( ! empty( $product_names ) ) {
|
||||
$item_data[] = array(
|
||||
'key' => __( 'Selected Products', 'wc-composable-product' ),
|
||||
'value' => implode( ', ', $product_names ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $item_data;
|
||||
}
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
$already_calculated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,227 +7,240 @@
|
||||
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Main plugin class - Singleton pattern
|
||||
*/
|
||||
class Plugin {
|
||||
/**
|
||||
* The single instance of the class
|
||||
*
|
||||
* @var Plugin
|
||||
*/
|
||||
protected static $instance = null;
|
||||
/**
|
||||
* The single instance of the class
|
||||
*
|
||||
* @var Plugin
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Twig environment
|
||||
*
|
||||
* @var \Twig\Environment
|
||||
*/
|
||||
private $twig = null;
|
||||
/**
|
||||
* Twig environment
|
||||
*
|
||||
* @var \Twig\Environment
|
||||
*/
|
||||
private $twig = null;
|
||||
|
||||
/**
|
||||
* Main Plugin Instance
|
||||
*
|
||||
* Ensures only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Plugin
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
/**
|
||||
* Main Plugin Instance
|
||||
*
|
||||
* Ensures only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Plugin
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( is_null( self::$instance ) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
$this->init_twig();
|
||||
$this->includes();
|
||||
}
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
$this->init_twig();
|
||||
$this->includes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WordPress and WooCommerce
|
||||
*/
|
||||
private function init_hooks() {
|
||||
// Register product type
|
||||
add_filter('product_type_selector', [$this, 'add_product_type']);
|
||||
add_filter('woocommerce_product_class', [$this, 'product_class'], 10, 2);
|
||||
/**
|
||||
* Hook into WordPress and WooCommerce
|
||||
*/
|
||||
private function init_hooks() {
|
||||
// Register product type
|
||||
add_filter( 'product_type_selector', array( $this, 'add_product_type' ) );
|
||||
add_filter( 'woocommerce_product_class', array( $this, 'product_class' ), 10, 2 );
|
||||
|
||||
// Enqueue scripts and styles
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_scripts']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
|
||||
// Enqueue scripts and styles
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
|
||||
|
||||
// Admin settings
|
||||
add_filter('woocommerce_get_settings_pages', [$this, 'add_settings_page']);
|
||||
}
|
||||
// Admin settings
|
||||
add_filter( 'woocommerce_get_settings_pages', array( $this, 'add_settings_page' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Twig template engine
|
||||
*/
|
||||
private function init_twig() {
|
||||
$loader = new \Twig\Loader\FilesystemLoader(WC_COMPOSABLE_PRODUCT_PATH . 'templates');
|
||||
$this->twig = new \Twig\Environment($loader, [
|
||||
'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache',
|
||||
'auto_reload' => true,
|
||||
'debug' => defined('WP_DEBUG') && WP_DEBUG,
|
||||
]);
|
||||
/**
|
||||
* Initialize Twig template engine
|
||||
*/
|
||||
private function init_twig() {
|
||||
$loader = new \Twig\Loader\FilesystemLoader( WC_COMPOSABLE_PRODUCT_PATH . 'templates' );
|
||||
$this->twig = new \Twig\Environment(
|
||||
$loader,
|
||||
array(
|
||||
'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache',
|
||||
'auto_reload' => true,
|
||||
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
|
||||
)
|
||||
);
|
||||
|
||||
// Add WordPress functions to Twig
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('__', function($text) {
|
||||
return __($text, 'wc-composable-product');
|
||||
}));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_html', 'esc_html'));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_attr', 'esc_attr'));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_url', 'esc_url'));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('wc_price', 'wc_price'));
|
||||
// Add WordPress functions to Twig
|
||||
$this->twig->addFunction(
|
||||
new \Twig\TwigFunction(
|
||||
'__',
|
||||
function ( $text ) {
|
||||
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- dynamic Twig template strings.
|
||||
return __( $text, 'wc-composable-product' );
|
||||
}
|
||||
)
|
||||
);
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_html', 'esc_html' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_attr', 'esc_attr' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_url', 'esc_url' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'wc_price', 'wc_price' ) );
|
||||
|
||||
// Add WordPress escaping functions as Twig filters
|
||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_html', 'esc_html'));
|
||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_attr', 'esc_attr'));
|
||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_url', 'esc_url'));
|
||||
}
|
||||
// Add WordPress escaping functions as Twig filters
|
||||
$this->twig->addFilter( new \Twig\TwigFilter( 'esc_html', 'esc_html' ) );
|
||||
$this->twig->addFilter( new \Twig\TwigFilter( 'esc_attr', 'esc_attr' ) );
|
||||
$this->twig->addFilter( new \Twig\TwigFilter( 'esc_url', 'esc_url' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Include required files
|
||||
*/
|
||||
private function includes() {
|
||||
// Note: Settings.php is NOT included here because it extends WC_Settings_Page
|
||||
// which isn't loaded until later. It's included in add_settings_page() instead.
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/ProductData.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductType.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/StockManager.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/CartHandler.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductSelector.php';
|
||||
/**
|
||||
* Include required files
|
||||
*/
|
||||
private function includes() {
|
||||
// Note: Settings.php is NOT included here because it extends WC_Settings_Page
|
||||
// which isn't loaded until later. It's included in add_settings_page() instead.
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/ProductData.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductType.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/StockManager.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/CartHandler.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductSelector.php';
|
||||
|
||||
// Initialize components
|
||||
new Admin\ProductData();
|
||||
new CartHandler();
|
||||
}
|
||||
// Initialize components
|
||||
new Admin\ProductData();
|
||||
new CartHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add composable product type to selector
|
||||
*
|
||||
* @param array $types Product types
|
||||
* @return array
|
||||
*/
|
||||
public function add_product_type($types) {
|
||||
$types['composable'] = __('Composable product', 'wc-composable-product');
|
||||
return $types;
|
||||
}
|
||||
/**
|
||||
* Add composable product type to selector
|
||||
*
|
||||
* @param array $types Product types
|
||||
* @return array
|
||||
*/
|
||||
public function add_product_type( $types ) {
|
||||
$types['composable'] = __( 'Composable product', 'wc-composable-product' );
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use custom product class for composable products
|
||||
*
|
||||
* @param string $classname Product class name
|
||||
* @param string $product_type Product type
|
||||
* @return string
|
||||
*/
|
||||
public function product_class($classname, $product_type) {
|
||||
if ($product_type === 'composable') {
|
||||
$classname = 'Magdev\WcComposableProduct\ProductType';
|
||||
}
|
||||
return $classname;
|
||||
}
|
||||
/**
|
||||
* Use custom product class for composable products
|
||||
*
|
||||
* @param string $classname Product class name
|
||||
* @param string $product_type Product type
|
||||
* @return string
|
||||
*/
|
||||
public function product_class( $classname, $product_type ) {
|
||||
if ( 'composable' === $product_type ) {
|
||||
$classname = 'Magdev\WcComposableProduct\ProductType';
|
||||
}
|
||||
return $classname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend scripts and styles
|
||||
*/
|
||||
public function enqueue_frontend_scripts() {
|
||||
if (is_product()) {
|
||||
wp_enqueue_style(
|
||||
'wc-composable-product',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css',
|
||||
[],
|
||||
WC_COMPOSABLE_PRODUCT_VERSION
|
||||
);
|
||||
/**
|
||||
* Enqueue frontend scripts and styles
|
||||
*/
|
||||
public function enqueue_frontend_scripts() {
|
||||
if ( is_product() ) {
|
||||
wp_enqueue_style(
|
||||
'wc-composable-product',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css',
|
||||
array(),
|
||||
WC_COMPOSABLE_PRODUCT_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wc-composable-product',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js',
|
||||
['jquery'],
|
||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||
true
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'wc-composable-product',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js',
|
||||
array( 'jquery' ),
|
||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script('wc-composable-product', 'wcComposableProduct', [
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('wc_composable_product_nonce'),
|
||||
'i18n' => [
|
||||
'select_items' => __('Please select items', 'wc-composable-product'),
|
||||
'max_items' => __('Maximum items selected', 'wc-composable-product'),
|
||||
'min_items' => __('Please select at least one item', 'wc-composable-product'),
|
||||
],
|
||||
'price_format' => [
|
||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||
'decimal_separator' => wc_get_price_decimal_separator(),
|
||||
'thousand_separator' => wc_get_price_thousand_separator(),
|
||||
'decimals' => wc_get_price_decimals(),
|
||||
'price_format' => get_woocommerce_price_format(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
wp_localize_script(
|
||||
'wc-composable-product',
|
||||
'wcComposableProduct',
|
||||
array(
|
||||
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'wc_composable_product_nonce' ),
|
||||
'i18n' => array(
|
||||
'select_items' => __( 'Please select items', 'wc-composable-product' ),
|
||||
'max_items' => __( 'Maximum items selected', 'wc-composable-product' ),
|
||||
'min_items' => __( 'Please select at least one item', 'wc-composable-product' ),
|
||||
),
|
||||
'price_format' => array(
|
||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||
'decimal_separator' => wc_get_price_decimal_separator(),
|
||||
'thousand_separator' => wc_get_price_thousand_separator(),
|
||||
'decimals' => wc_get_price_decimals(),
|
||||
'price_format' => get_woocommerce_price_format(),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin scripts and styles
|
||||
*/
|
||||
public function enqueue_admin_scripts($hook) {
|
||||
if ('post.php' === $hook || 'post-new.php' === $hook) {
|
||||
global $post_type;
|
||||
if ('product' === $post_type) {
|
||||
wp_enqueue_style(
|
||||
'wc-composable-product-admin',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css',
|
||||
[],
|
||||
WC_COMPOSABLE_PRODUCT_VERSION
|
||||
);
|
||||
/**
|
||||
* Enqueue admin scripts and styles
|
||||
*/
|
||||
public function enqueue_admin_scripts( $hook ) {
|
||||
if ( 'post.php' === $hook || 'post-new.php' === $hook ) {
|
||||
global $post_type;
|
||||
if ( 'product' === $post_type ) {
|
||||
wp_enqueue_style(
|
||||
'wc-composable-product-admin',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css',
|
||||
array(),
|
||||
WC_COMPOSABLE_PRODUCT_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wc-composable-product-admin',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js',
|
||||
['jquery', 'wc-admin-product-meta-boxes'],
|
||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
wp_enqueue_script(
|
||||
'wc-composable-product-admin',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js',
|
||||
array( 'jquery', 'wc-admin-product-meta-boxes' ),
|
||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add settings page to WooCommerce
|
||||
*
|
||||
* @param array $settings WooCommerce settings pages
|
||||
* @return array
|
||||
*/
|
||||
public function add_settings_page($settings) {
|
||||
// Include Settings.php here, when WC_Settings_Page is guaranteed to be loaded
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
||||
$settings[] = new Admin\Settings();
|
||||
return $settings;
|
||||
}
|
||||
/**
|
||||
* Add settings page to WooCommerce
|
||||
*
|
||||
* @param array $settings WooCommerce settings pages
|
||||
* @return array
|
||||
*/
|
||||
public function add_settings_page( $settings ) {
|
||||
// Include Settings.php here, when WC_Settings_Page is guaranteed to be loaded
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
||||
$settings[] = new Admin\Settings();
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twig environment
|
||||
*
|
||||
* @return \Twig\Environment
|
||||
*/
|
||||
public function get_twig() {
|
||||
return $this->twig;
|
||||
}
|
||||
/**
|
||||
* Get Twig environment
|
||||
*
|
||||
* @return \Twig\Environment
|
||||
*/
|
||||
public function get_twig() {
|
||||
return $this->twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Twig template
|
||||
*
|
||||
* @param string $template Template name
|
||||
* @param array $context Template variables
|
||||
* @return string
|
||||
*/
|
||||
public function render_template($template, $context = []) {
|
||||
return $this->twig->render($template, $context);
|
||||
}
|
||||
/**
|
||||
* Render a Twig template
|
||||
*
|
||||
* @param string $template Template name
|
||||
* @param array $context Template variables
|
||||
* @return string
|
||||
*/
|
||||
public function render_template( $template, $context = array() ) {
|
||||
return $this->twig->render( $template, $context );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Product Selector Class
|
||||
@@ -15,63 +15,64 @@ defined('ABSPATH') || exit;
|
||||
* Handles rendering the product selection interface
|
||||
*/
|
||||
class ProductSelector {
|
||||
/**
|
||||
* Render product selector
|
||||
*
|
||||
* @param ProductType $product Composable product
|
||||
*/
|
||||
public static function render($product) {
|
||||
if (!$product || $product->get_type() !== 'composable') {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Render product selector
|
||||
*
|
||||
* @param ProductType $product Composable product
|
||||
*/
|
||||
public static function render( $product ) {
|
||||
if ( ! $product || $product->get_type() !== 'composable' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$available_products = $product->get_available_products();
|
||||
$selection_limit = $product->get_selection_limit();
|
||||
$pricing_mode = $product->get_pricing_mode();
|
||||
$available_products = $product->get_available_products();
|
||||
$selection_limit = $product->get_selection_limit();
|
||||
$pricing_mode = $product->get_pricing_mode();
|
||||
|
||||
$show_images = get_option('wc_composable_show_images', 'yes') === 'yes';
|
||||
$show_prices = get_option('wc_composable_show_prices', 'yes') === 'yes';
|
||||
$show_total = get_option('wc_composable_show_total', 'yes') === 'yes';
|
||||
$show_images = get_option( 'wc_composable_show_images', 'yes' ) === 'yes';
|
||||
$show_prices = get_option( 'wc_composable_show_prices', 'yes' ) === 'yes';
|
||||
$show_total = get_option( 'wc_composable_show_total', 'yes' ) === 'yes';
|
||||
|
||||
// Get stock manager for stock information
|
||||
$stock_manager = new StockManager();
|
||||
// Get stock manager for stock information
|
||||
$stock_manager = new StockManager();
|
||||
|
||||
// Prepare product data for template
|
||||
$products_data = [];
|
||||
foreach ($available_products as $available_product) {
|
||||
$stock_info = $stock_manager->get_product_stock_info($available_product->get_id());
|
||||
// Prepare product data for template
|
||||
$products_data = array();
|
||||
foreach ( $available_products as $available_product ) {
|
||||
$stock_info = $stock_manager->get_product_stock_info( $available_product->get_id() );
|
||||
|
||||
$products_data[] = [
|
||||
'id' => $available_product->get_id(),
|
||||
'name' => $available_product->get_name(),
|
||||
'price' => $available_product->get_price(),
|
||||
'price_html' => $available_product->get_price_html(),
|
||||
'image_url' => wp_get_attachment_image_url($available_product->get_image_id(), 'thumbnail'),
|
||||
'permalink' => $available_product->get_permalink(),
|
||||
'stock_status' => $stock_info['stock_status'],
|
||||
'in_stock' => $stock_info['in_stock'],
|
||||
'stock_quantity' => $stock_info['stock_quantity'],
|
||||
'managing_stock' => $stock_info['managing_stock'],
|
||||
'backorders_allowed' => $stock_info['backorders_allowed'],
|
||||
];
|
||||
}
|
||||
$products_data[] = array(
|
||||
'id' => $available_product->get_id(),
|
||||
'name' => $available_product->get_name(),
|
||||
'price' => $available_product->get_price(),
|
||||
'price_html' => $available_product->get_price_html(),
|
||||
'image_url' => wp_get_attachment_image_url( $available_product->get_image_id(), 'thumbnail' ),
|
||||
'permalink' => $available_product->get_permalink(),
|
||||
'stock_status' => $stock_info['stock_status'],
|
||||
'in_stock' => $stock_info['in_stock'],
|
||||
'stock_quantity' => $stock_info['stock_quantity'],
|
||||
'managing_stock' => $stock_info['managing_stock'],
|
||||
'backorders_allowed' => $stock_info['backorders_allowed'],
|
||||
);
|
||||
}
|
||||
|
||||
$context = [
|
||||
'product_id' => $product->get_id(),
|
||||
'products' => $products_data,
|
||||
'selection_limit' => $selection_limit,
|
||||
'pricing_mode' => $pricing_mode,
|
||||
'show_images' => $show_images,
|
||||
'show_prices' => $show_prices,
|
||||
'show_total' => $show_total,
|
||||
'fixed_price' => $product->get_price(),
|
||||
'fixed_price_html' => wc_price($product->get_price()),
|
||||
'zero_price_html' => wc_price(0),
|
||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||
];
|
||||
$context = array(
|
||||
'product_id' => $product->get_id(),
|
||||
'products' => $products_data,
|
||||
'selection_limit' => $selection_limit,
|
||||
'pricing_mode' => $pricing_mode,
|
||||
'show_images' => $show_images,
|
||||
'show_prices' => $show_prices,
|
||||
'show_total' => $show_total,
|
||||
'fixed_price' => $product->get_price(),
|
||||
'fixed_price_html' => wc_price( $product->get_price() ),
|
||||
'zero_price_html' => wc_price( 0 ),
|
||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||
);
|
||||
|
||||
// Render template
|
||||
$plugin = Plugin::instance();
|
||||
echo $plugin->render_template('product-selector.twig', $context);
|
||||
}
|
||||
// Render template — Twig handles escaping via registered esc_html/esc_attr/esc_url functions.
|
||||
$plugin = Plugin::instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped by Twig template.
|
||||
echo $plugin->render_template( 'product-selector.twig', $context );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,243 +7,248 @@
|
||||
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Composable Product Type Class
|
||||
*/
|
||||
class ProductType extends \WC_Product {
|
||||
/**
|
||||
* Product type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $product_type = 'composable';
|
||||
/**
|
||||
* Product type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $product_type = 'composable';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param mixed $product Product ID or object
|
||||
*/
|
||||
public function __construct($product = 0) {
|
||||
$this->supports[] = 'ajax_add_to_cart';
|
||||
parent::__construct($product);
|
||||
}
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param mixed $product Product ID or object
|
||||
*/
|
||||
public function __construct( $product = 0 ) {
|
||||
$this->supports[] = 'ajax_add_to_cart';
|
||||
parent::__construct( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type() {
|
||||
return 'composable';
|
||||
}
|
||||
/**
|
||||
* Get product type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type() {
|
||||
return 'composable';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selection limit
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_selection_limit() {
|
||||
$limit = $this->get_meta('_composable_selection_limit', true);
|
||||
if (empty($limit)) {
|
||||
$limit = get_option('wc_composable_default_limit', 5);
|
||||
}
|
||||
return absint($limit);
|
||||
}
|
||||
/**
|
||||
* Get selection limit
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_selection_limit() {
|
||||
$limit = $this->get_meta( '_composable_selection_limit', true );
|
||||
if ( empty( $limit ) ) {
|
||||
$limit = get_option( 'wc_composable_default_limit', 5 );
|
||||
}
|
||||
return absint( $limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pricing mode
|
||||
*
|
||||
* @return string 'fixed' or 'sum'
|
||||
*/
|
||||
public function get_pricing_mode() {
|
||||
$mode = $this->get_meta('_composable_pricing_mode', true);
|
||||
if (empty($mode)) {
|
||||
$mode = get_option('wc_composable_default_pricing', 'sum');
|
||||
}
|
||||
return $mode;
|
||||
}
|
||||
/**
|
||||
* Get pricing mode
|
||||
*
|
||||
* @return string 'fixed' or 'sum'
|
||||
*/
|
||||
public function get_pricing_mode() {
|
||||
$mode = $this->get_meta( '_composable_pricing_mode', true );
|
||||
if ( empty( $mode ) ) {
|
||||
$mode = get_option( 'wc_composable_default_pricing', 'sum' );
|
||||
}
|
||||
return $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product selection criteria
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_selection_criteria() {
|
||||
return [
|
||||
'type' => $this->get_meta('_composable_criteria_type', true) ?: 'category',
|
||||
'categories' => $this->get_meta('_composable_categories', true) ?: [],
|
||||
'tags' => $this->get_meta('_composable_tags', true) ?: [],
|
||||
'skus' => $this->get_meta('_composable_skus', true) ?: '',
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Get product selection criteria
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_selection_criteria() {
|
||||
$type = $this->get_meta( '_composable_criteria_type', true );
|
||||
$categories = $this->get_meta( '_composable_categories', true );
|
||||
$tags = $this->get_meta( '_composable_tags', true );
|
||||
$skus = $this->get_meta( '_composable_skus', true );
|
||||
|
||||
/**
|
||||
* Check if product is purchasable
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable() {
|
||||
return true;
|
||||
}
|
||||
return array(
|
||||
'type' => $type ? $type : 'category',
|
||||
'categories' => $categories ? $categories : array(),
|
||||
'tags' => $tags ? $tags : array(),
|
||||
'skus' => $skus ? $skus : '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if product is sold individually
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_sold_individually() {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Check if product is purchasable
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if non-public products should be included
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_include_unpublished() {
|
||||
$per_product = $this->get_meta('_composable_include_unpublished', true);
|
||||
if ($per_product === 'yes') {
|
||||
return true;
|
||||
}
|
||||
if ($per_product === 'no') {
|
||||
return false;
|
||||
}
|
||||
return get_option('wc_composable_include_unpublished', 'no') === 'yes';
|
||||
}
|
||||
/**
|
||||
* Check if product is sold individually
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_sold_individually() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available products based on criteria
|
||||
*
|
||||
* @return array Array of WC_Product objects
|
||||
*/
|
||||
public function get_available_products() {
|
||||
$criteria = $this->get_selection_criteria();
|
||||
$include_unpublished = $this->should_include_unpublished();
|
||||
$args = [
|
||||
'post_type' => 'product',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => $include_unpublished ? ['publish', 'draft', 'private'] : 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
];
|
||||
/**
|
||||
* Check if non-public products should be included
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_include_unpublished() {
|
||||
$per_product = $this->get_meta( '_composable_include_unpublished', true );
|
||||
if ( 'yes' === $per_product ) {
|
||||
return true;
|
||||
}
|
||||
if ( 'no' === $per_product ) {
|
||||
return false;
|
||||
}
|
||||
return 'yes' === get_option( 'wc_composable_include_unpublished', 'no' );
|
||||
}
|
||||
|
||||
// Exclude composable products using the product_type taxonomy
|
||||
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
||||
$args['tax_query'] = [
|
||||
'relation' => 'AND',
|
||||
[
|
||||
'taxonomy' => 'product_type',
|
||||
'field' => 'slug',
|
||||
'terms' => ['composable'],
|
||||
'operator' => 'NOT IN',
|
||||
],
|
||||
];
|
||||
/**
|
||||
* Get available products based on criteria
|
||||
*
|
||||
* @return array Array of WC_Product objects
|
||||
*/
|
||||
public function get_available_products() {
|
||||
$criteria = $this->get_selection_criteria();
|
||||
$include_unpublished = $this->should_include_unpublished();
|
||||
$args = array(
|
||||
'post_type' => 'product',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => $include_unpublished ? array( 'publish', 'draft', 'private' ) : 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
);
|
||||
|
||||
switch ($criteria['type']) {
|
||||
case 'category':
|
||||
if (!empty($criteria['categories'])) {
|
||||
$args['tax_query'][] = [
|
||||
'taxonomy' => 'product_cat',
|
||||
'field' => 'term_id',
|
||||
'terms' => $criteria['categories'],
|
||||
'operator' => 'IN',
|
||||
];
|
||||
}
|
||||
break;
|
||||
// Exclude composable products using the product_type taxonomy
|
||||
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
||||
$args['tax_query'] = array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'taxonomy' => 'product_type',
|
||||
'field' => 'slug',
|
||||
'terms' => array( 'composable' ),
|
||||
'operator' => 'NOT IN',
|
||||
),
|
||||
);
|
||||
|
||||
case 'tag':
|
||||
if (!empty($criteria['tags'])) {
|
||||
$args['tax_query'][] = [
|
||||
'taxonomy' => 'product_tag',
|
||||
'field' => 'term_id',
|
||||
'terms' => $criteria['tags'],
|
||||
'operator' => 'IN',
|
||||
];
|
||||
}
|
||||
break;
|
||||
switch ( $criteria['type'] ) {
|
||||
case 'category':
|
||||
if ( ! empty( $criteria['categories'] ) ) {
|
||||
$args['tax_query'][] = array(
|
||||
'taxonomy' => 'product_cat',
|
||||
'field' => 'term_id',
|
||||
'terms' => $criteria['categories'],
|
||||
'operator' => 'IN',
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sku':
|
||||
if (!empty($criteria['skus'])) {
|
||||
$skus = array_map('trim', explode(',', $criteria['skus']));
|
||||
$args['meta_query'] = [
|
||||
[
|
||||
'key' => '_sku',
|
||||
'value' => $skus,
|
||||
'compare' => 'IN',
|
||||
],
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tag':
|
||||
if ( ! empty( $criteria['tags'] ) ) {
|
||||
$args['tax_query'][] = array(
|
||||
'taxonomy' => 'product_tag',
|
||||
'field' => 'term_id',
|
||||
'terms' => $criteria['tags'],
|
||||
'operator' => 'IN',
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
$query = new \WP_Query($args);
|
||||
$products = [];
|
||||
case 'sku':
|
||||
if ( ! empty( $criteria['skus'] ) ) {
|
||||
$skus = array_map( 'trim', explode( ',', $criteria['skus'] ) );
|
||||
$args['meta_query'] = array(
|
||||
array(
|
||||
'key' => '_sku',
|
||||
'value' => $skus,
|
||||
'compare' => 'IN',
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($query->have_posts()) {
|
||||
foreach ($query->posts as $post) {
|
||||
$product = wc_get_product($post->ID);
|
||||
$query = new \WP_Query( $args );
|
||||
$products = array();
|
||||
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
if ( $query->have_posts() ) {
|
||||
foreach ( $query->posts as $post ) {
|
||||
$product = wc_get_product( $post->ID );
|
||||
|
||||
// Handle variable products by including their variations
|
||||
if ($product->is_type('variable')) {
|
||||
$variation_ids = $product->get_children();
|
||||
foreach ($variation_ids as $variation_id) {
|
||||
$variation = wc_get_product($variation_id);
|
||||
if ($variation && ($include_unpublished || $variation->is_purchasable())) {
|
||||
$products[] = $variation;
|
||||
}
|
||||
}
|
||||
} elseif ($include_unpublished || $product->is_purchasable()) {
|
||||
$products[] = $product;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( ! $product ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wp_reset_postdata();
|
||||
// Handle variable products by including their variations
|
||||
if ( $product->is_type( 'variable' ) ) {
|
||||
$variation_ids = $product->get_children();
|
||||
foreach ( $variation_ids as $variation_id ) {
|
||||
$variation = wc_get_product( $variation_id );
|
||||
if ( $variation && ( $include_unpublished || $variation->is_purchasable() ) ) {
|
||||
$products[] = $variation;
|
||||
}
|
||||
}
|
||||
} elseif ( $include_unpublished || $product->is_purchasable() ) {
|
||||
$products[] = $product;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
wp_reset_postdata();
|
||||
|
||||
/**
|
||||
* Calculate price based on selected products
|
||||
*
|
||||
* @param array $selected_products Array of product IDs
|
||||
* @return float
|
||||
*/
|
||||
public function calculate_composed_price($selected_products) {
|
||||
$pricing_mode = $this->get_pricing_mode();
|
||||
return $products;
|
||||
}
|
||||
|
||||
if ($pricing_mode === 'fixed') {
|
||||
return floatval($this->get_regular_price());
|
||||
}
|
||||
/**
|
||||
* Calculate price based on selected products
|
||||
*
|
||||
* @param array $selected_products Array of product IDs
|
||||
* @return float
|
||||
*/
|
||||
public function calculate_composed_price( $selected_products ) {
|
||||
$pricing_mode = $this->get_pricing_mode();
|
||||
|
||||
$total = 0;
|
||||
foreach ($selected_products as $product_id) {
|
||||
$product = wc_get_product($product_id);
|
||||
if ($product) {
|
||||
$total += floatval($product->get_price());
|
||||
}
|
||||
}
|
||||
if ( 'fixed' === $pricing_mode ) {
|
||||
return floatval( $this->get_regular_price() );
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
$total = 0;
|
||||
foreach ( $selected_products as $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( $product ) {
|
||||
$total += floatval( $product->get_price() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to cart validation
|
||||
*
|
||||
* @param int $product_id Product ID
|
||||
* @param int $quantity Quantity
|
||||
* @param int $variation_id Variation ID
|
||||
* @param array $variations Variations
|
||||
* @param array $cart_item_data Cart item data
|
||||
* @return bool
|
||||
*/
|
||||
public function add_to_cart_validation($product_id, $quantity, $variation_id = 0, $variations = [], $cart_item_data = []) {
|
||||
return true;
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to cart validation
|
||||
*
|
||||
* @param int $product_id Product ID
|
||||
* @param int $quantity Quantity
|
||||
* @param int $variation_id Variation ID
|
||||
* @param array $variations Variations
|
||||
* @param array $cart_item_data Cart item data
|
||||
* @return bool
|
||||
*/
|
||||
public function add_to_cart_validation( $product_id, $quantity, $variation_id = 0, $variations = array(), $cart_item_data = array() ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace Magdev\WcComposableProduct;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Stock Manager Class
|
||||
@@ -20,15 +20,15 @@ class StockManager {
|
||||
*/
|
||||
public function __construct() {
|
||||
// Hook into order completion to reduce stock
|
||||
add_action('woocommerce_order_status_completed', [$this, 'reduce_stock_on_order_complete'], 10, 1);
|
||||
add_action('woocommerce_order_status_processing', [$this, 'reduce_stock_on_order_complete'], 10, 1);
|
||||
add_action( 'woocommerce_order_status_completed', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 );
|
||||
add_action( 'woocommerce_order_status_processing', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 );
|
||||
|
||||
// Hook into order cancellation/refund to restore stock
|
||||
add_action('woocommerce_order_status_cancelled', [$this, 'restore_stock_on_order_cancel'], 10, 1);
|
||||
add_action('woocommerce_order_status_refunded', [$this, 'restore_stock_on_order_cancel'], 10, 1);
|
||||
add_action( 'woocommerce_order_status_cancelled', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 );
|
||||
add_action( 'woocommerce_order_status_refunded', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 );
|
||||
|
||||
// Prevent double stock reduction
|
||||
add_filter('woocommerce_can_reduce_order_stock', [$this, 'prevent_composable_stock_reduction'], 10, 2);
|
||||
add_filter( 'woocommerce_can_reduce_order_stock', array( $this, 'prevent_composable_stock_reduction' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,42 +38,42 @@ class StockManager {
|
||||
* @param int $quantity Quantity of composable product being added
|
||||
* @return bool|string True if in stock, error message otherwise
|
||||
*/
|
||||
public function validate_stock_availability($selected_product_ids, $quantity = 1) {
|
||||
foreach ($selected_product_ids as $product_id) {
|
||||
$product = wc_get_product($product_id);
|
||||
public function validate_stock_availability( $selected_product_ids, $quantity = 1 ) {
|
||||
foreach ( $selected_product_ids as $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if (!$product) {
|
||||
if ( ! $product ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip stock check if stock management is disabled for this product
|
||||
if (!$product->managing_stock()) {
|
||||
if ( ! $product->managing_stock() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stock_quantity = $product->get_stock_quantity();
|
||||
|
||||
// Check if product is in stock
|
||||
if (!$product->is_in_stock()) {
|
||||
if ( ! $product->is_in_stock() ) {
|
||||
return sprintf(
|
||||
/* translators: %s: product name */
|
||||
__('"%s" is out of stock and cannot be selected.', 'wc-composable-product'),
|
||||
__( '"%s" is out of stock and cannot be selected.', 'wc-composable-product' ),
|
||||
$product->get_name()
|
||||
);
|
||||
}
|
||||
|
||||
// Check if enough stock is available
|
||||
if ($stock_quantity !== null && $stock_quantity < $quantity) {
|
||||
if ( null !== $stock_quantity && $stock_quantity < $quantity ) {
|
||||
return sprintf(
|
||||
/* translators: 1: product name, 2: stock quantity */
|
||||
__('Only %2$d of "%1$s" are available in stock.', 'wc-composable-product'),
|
||||
__( 'Only %2$d of "%1$s" are available in stock.', 'wc-composable-product' ),
|
||||
$product->get_name(),
|
||||
$stock_quantity
|
||||
);
|
||||
}
|
||||
|
||||
// Check for backorders
|
||||
if ($product->backorders_allowed()) {
|
||||
if ( $product->backorders_allowed() ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -88,29 +88,29 @@ class StockManager {
|
||||
* @param int $required_quantity Required quantity
|
||||
* @return array Stock information [in_stock, stock_quantity, backorders_allowed]
|
||||
*/
|
||||
public function get_product_stock_info($product_id, $required_quantity = 1) {
|
||||
$product = wc_get_product($product_id);
|
||||
public function get_product_stock_info( $product_id, $required_quantity = 1 ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if (!$product) {
|
||||
return [
|
||||
'in_stock' => false,
|
||||
'stock_quantity' => 0,
|
||||
if ( ! $product ) {
|
||||
return array(
|
||||
'in_stock' => false,
|
||||
'stock_quantity' => 0,
|
||||
'backorders_allowed' => false,
|
||||
'stock_status' => 'outofstock',
|
||||
];
|
||||
'stock_status' => 'outofstock',
|
||||
);
|
||||
}
|
||||
|
||||
$stock_quantity = $product->get_stock_quantity();
|
||||
$managing_stock = $product->managing_stock();
|
||||
|
||||
return [
|
||||
'in_stock' => $product->is_in_stock(),
|
||||
'stock_quantity' => $stock_quantity,
|
||||
return array(
|
||||
'in_stock' => $product->is_in_stock(),
|
||||
'stock_quantity' => $stock_quantity,
|
||||
'backorders_allowed' => $product->backorders_allowed(),
|
||||
'stock_status' => $product->get_stock_status(),
|
||||
'managing_stock' => $managing_stock,
|
||||
'has_enough_stock' => !$managing_stock || $stock_quantity === null || $stock_quantity >= $required_quantity,
|
||||
];
|
||||
'stock_status' => $product->get_stock_status(),
|
||||
'managing_stock' => $managing_stock,
|
||||
'has_enough_stock' => ! $managing_stock || null === $stock_quantity || $stock_quantity >= $required_quantity,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,54 +118,54 @@ class StockManager {
|
||||
*
|
||||
* @param int $order_id Order ID
|
||||
*/
|
||||
public function reduce_stock_on_order_complete($order_id) {
|
||||
$order = wc_get_order($order_id);
|
||||
public function reduce_stock_on_order_complete( $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if (!$order) {
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if stock has already been reduced
|
||||
if ($order->get_meta('_composable_stock_reduced', true)) {
|
||||
if ( $order->get_meta( '_composable_stock_reduced', true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($order->get_items() as $item) {
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$product = $item->get_product();
|
||||
|
||||
if (!$product || $product->get_type() !== 'composable') {
|
||||
if ( ! $product || $product->get_type() !== 'composable' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get selected products from order item meta
|
||||
$selected_products = $item->get_meta('_composable_products', true);
|
||||
$selected_products = $item->get_meta( '_composable_products', true );
|
||||
|
||||
if (empty($selected_products) || !is_array($selected_products)) {
|
||||
if ( empty( $selected_products ) || ! is_array( $selected_products ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quantity = $item->get_quantity();
|
||||
|
||||
// Reduce stock for each selected product
|
||||
foreach ($selected_products as $product_id) {
|
||||
$selected_product = wc_get_product($product_id);
|
||||
foreach ( $selected_products as $product_id ) {
|
||||
$selected_product = wc_get_product( $product_id );
|
||||
|
||||
if (!$selected_product || !$selected_product->managing_stock()) {
|
||||
if ( ! $selected_product || ! $selected_product->managing_stock() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stock_quantity = $selected_product->get_stock_quantity();
|
||||
|
||||
if ($stock_quantity !== null) {
|
||||
if ( null !== $stock_quantity ) {
|
||||
$new_stock = $stock_quantity - $quantity;
|
||||
$selected_product->set_stock_quantity($new_stock);
|
||||
$selected_product->set_stock_quantity( $new_stock );
|
||||
$selected_product->save();
|
||||
|
||||
// Add order note
|
||||
$order->add_order_note(
|
||||
sprintf(
|
||||
/* translators: 1: product name, 2: quantity, 3: remaining stock */
|
||||
__('Stock reduced for "%1$s": -%2$d (remaining: %3$d)', 'wc-composable-product'),
|
||||
__( 'Stock reduced for "%1$s": -%2$d (remaining: %3$d)', 'wc-composable-product' ),
|
||||
$selected_product->get_name(),
|
||||
$quantity,
|
||||
$new_stock
|
||||
@@ -176,7 +176,7 @@ class StockManager {
|
||||
}
|
||||
|
||||
// Mark stock as reduced
|
||||
$order->update_meta_data('_composable_stock_reduced', true);
|
||||
$order->update_meta_data( '_composable_stock_reduced', true );
|
||||
$order->save();
|
||||
}
|
||||
|
||||
@@ -185,54 +185,54 @@ class StockManager {
|
||||
*
|
||||
* @param int $order_id Order ID
|
||||
*/
|
||||
public function restore_stock_on_order_cancel($order_id) {
|
||||
$order = wc_get_order($order_id);
|
||||
public function restore_stock_on_order_cancel( $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if (!$order) {
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if stock was reduced
|
||||
if (!$order->get_meta('_composable_stock_reduced', true)) {
|
||||
if ( ! $order->get_meta( '_composable_stock_reduced', true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($order->get_items() as $item) {
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$product = $item->get_product();
|
||||
|
||||
if (!$product || $product->get_type() !== 'composable') {
|
||||
if ( ! $product || $product->get_type() !== 'composable' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get selected products from order item meta
|
||||
$selected_products = $item->get_meta('_composable_products', true);
|
||||
$selected_products = $item->get_meta( '_composable_products', true );
|
||||
|
||||
if (empty($selected_products) || !is_array($selected_products)) {
|
||||
if ( empty( $selected_products ) || ! is_array( $selected_products ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quantity = $item->get_quantity();
|
||||
|
||||
// Restore stock for each selected product
|
||||
foreach ($selected_products as $product_id) {
|
||||
$selected_product = wc_get_product($product_id);
|
||||
foreach ( $selected_products as $product_id ) {
|
||||
$selected_product = wc_get_product( $product_id );
|
||||
|
||||
if (!$selected_product || !$selected_product->managing_stock()) {
|
||||
if ( ! $selected_product || ! $selected_product->managing_stock() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stock_quantity = $selected_product->get_stock_quantity();
|
||||
|
||||
if ($stock_quantity !== null) {
|
||||
if ( null !== $stock_quantity ) {
|
||||
$new_stock = $stock_quantity + $quantity;
|
||||
$selected_product->set_stock_quantity($new_stock);
|
||||
$selected_product->set_stock_quantity( $new_stock );
|
||||
$selected_product->save();
|
||||
|
||||
// Add order note
|
||||
$order->add_order_note(
|
||||
sprintf(
|
||||
/* translators: 1: product name, 2: quantity, 3: new stock */
|
||||
__('Stock restored for "%1$s": +%2$d (total: %3$d)', 'wc-composable-product'),
|
||||
__( 'Stock restored for "%1$s": +%2$d (total: %3$d)', 'wc-composable-product' ),
|
||||
$selected_product->get_name(),
|
||||
$quantity,
|
||||
$new_stock
|
||||
@@ -243,7 +243,7 @@ class StockManager {
|
||||
}
|
||||
|
||||
// Mark stock as restored
|
||||
$order->update_meta_data('_composable_stock_reduced', false);
|
||||
$order->update_meta_data( '_composable_stock_reduced', false );
|
||||
$order->save();
|
||||
}
|
||||
|
||||
@@ -255,11 +255,11 @@ class StockManager {
|
||||
* @param \WC_Order $order Order object
|
||||
* @return bool
|
||||
*/
|
||||
public function prevent_composable_stock_reduction($reduce_stock, $order) {
|
||||
foreach ($order->get_items() as $item) {
|
||||
public function prevent_composable_stock_reduction( $reduce_stock, $order ) {
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$product = $item->get_product();
|
||||
|
||||
if ($product && $product->get_type() === 'composable') {
|
||||
if ( $product && $product->get_type() === 'composable' ) {
|
||||
// We'll handle stock reduction manually
|
||||
return false;
|
||||
}
|
||||
@@ -275,9 +275,9 @@ class StockManager {
|
||||
* @param string $cart_item_key Cart item key
|
||||
* @param array $values Cart item values
|
||||
*/
|
||||
public function store_selected_products_in_order($item, $cart_item_key, $values) {
|
||||
if (isset($values['composable_products']) && !empty($values['composable_products'])) {
|
||||
$item->add_meta_data('_composable_products', $values['composable_products'], true);
|
||||
public function store_selected_products_in_order( $item, $cart_item_key, $values ) {
|
||||
if ( isset( $values['composable_products'] ) && ! empty( $values['composable_products'] ) ) {
|
||||
$item->add_meta_data( '_composable_products', $values['composable_products'], true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user