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:
@@ -28,7 +28,30 @@ jobs:
|
|||||||
find includes -name "*.php" -print0 | xargs -0 -n1 php -l
|
find includes -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||||
find tests -name "*.php" -print0 | xargs -0 -n1 php -l
|
find tests -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||||
|
|
||||||
|
phpcs:
|
||||||
|
name: PHP CodeSniffer
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, xml
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Install Composer dependencies
|
||||||
|
run: |
|
||||||
|
composer config platform.php 8.3.0
|
||||||
|
composer install --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
- name: Run PHPCS
|
||||||
|
run: vendor/bin/phpcs
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
name: PHP Unit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: lint
|
needs: lint
|
||||||
steps:
|
steps:
|
||||||
@@ -51,7 +74,7 @@ jobs:
|
|||||||
run: vendor/bin/phpunit --testdox
|
run: vendor/bin/phpunit --testdox
|
||||||
|
|
||||||
build-release:
|
build-release:
|
||||||
needs: test
|
needs: [test, phpcs]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,3 +25,4 @@ phpunit.xml
|
|||||||
languages/*.mo
|
languages/*.mo
|
||||||
|
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
.phpunit.cache/
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -12,15 +12,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **PHPUnit test suite**: 57 unit tests covering all 6 core classes (ProductType, StockManager, CartHandler, Plugin, Admin/ProductData, Admin/Settings)
|
- **PHPUnit test suite**: 57 unit tests covering all 6 core classes (ProductType, StockManager, CartHandler, Plugin, Admin/ProductData, Admin/Settings)
|
||||||
- **Brain Monkey + Mockery** for WordPress/WooCommerce function mocking without a full WP installation
|
- **Brain Monkey + Mockery** for WordPress/WooCommerce function mocking without a full WP installation
|
||||||
- **WooCommerce class stubs** in `tests/stubs/` for classes extended by the plugin (WC_Product, WC_Settings_Page, etc.)
|
- **WooCommerce class stubs** in `tests/stubs/` for classes extended by the plugin (WC_Product, WC_Settings_Page, etc.)
|
||||||
|
- **PHPCS** with WordPress-Extra and PHPCompatibilityWP coding standards (`phpcs.xml.dist`)
|
||||||
|
- **PHPCS job** in release workflow — coding standards must pass before release is built
|
||||||
- **PHP lint job** in release workflow — syntax-checks all PHP files before testing
|
- **PHP lint job** in release workflow — syntax-checks all PHP files before testing
|
||||||
- **Test job** in release workflow — tests must pass before release package is built (`needs: test`)
|
- **Test job** in release workflow — tests must pass before release package is built
|
||||||
- Testing section in README
|
- Testing and linting sections in README and CLAUDE.md
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **PSR-4 refactored**: Renamed files to PascalCase (Product_Type → ProductType, etc.) and changed namespace from `WC_Composable_Product` to `Magdev\WcComposableProduct`
|
- **PSR-4 refactored**: Renamed files to PascalCase (Product_Type → ProductType, etc.) and changed namespace from `WC_Composable_Product` to `Magdev\WcComposableProduct`
|
||||||
- Updated all cross-references in PHP files, main plugin file, composer.json, CSS/JS doc comments, and translation file source comments
|
- Updated all cross-references in PHP files, main plugin file, composer.json, CSS/JS doc comments, and translation file source comments
|
||||||
- Release workflow now has three stages: lint → test → build-release
|
- **PHPUnit upgraded** from 9.6 to 10 (Brain Monkey 2.7 supports both)
|
||||||
|
- **WPCS formatting** applied to all source files (tabs, Yoda conditions, strict `in_array`, `wp_json_encode`, long array syntax)
|
||||||
|
- Release workflow now has four stages: lint + phpcs (parallel) → test → build-release
|
||||||
|
- Composer platform pinned to PHP 8.3 to prevent incompatible dependency locks
|
||||||
|
|
||||||
## [1.3.0] - 2026-03-01
|
## [1.3.0] - 2026-03-01
|
||||||
|
|
||||||
|
|||||||
12
CLAUDE.md
12
CLAUDE.md
@@ -18,7 +18,8 @@ This project is 100% AI-generated ("vibe-coded") using Claude.AI.
|
|||||||
- **Styling:** Custom CSS
|
- **Styling:** Custom CSS
|
||||||
- **Dependencies:** Composer
|
- **Dependencies:** Composer
|
||||||
- **i18n:** WordPress i18n (.pot/.po/.mo), text domain: `wc-composable-product`
|
- **i18n:** WordPress i18n (.pot/.po/.mo), text domain: `wc-composable-product`
|
||||||
- **Testing:** PHPUnit 9.6 + Brain Monkey 2.7 + Mockery 1.6
|
- **Testing:** PHPUnit 10 + Brain Monkey 2.7 + Mockery 1.6
|
||||||
|
- **Linting:** PHPCS 3.13 with WordPress-Extra + PHPCompatibilityWP
|
||||||
- **CI/CD:** Gitea Actions (`.gitea/workflows/release.yml`)
|
- **CI/CD:** Gitea Actions (`.gitea/workflows/release.yml`)
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
@@ -55,6 +56,7 @@ wc-composable-product/
|
|||||||
│ └── product-selector.twig # Frontend selection interface
|
│ └── product-selector.twig # Frontend selection interface
|
||||||
├── vendor/ # Composer dependencies (gitignored, included in releases)
|
├── vendor/ # Composer dependencies (gitignored, included in releases)
|
||||||
├── composer.json
|
├── composer.json
|
||||||
|
├── phpcs.xml.dist # PHPCS configuration (WordPress-Extra + PHPCompatibilityWP)
|
||||||
├── phpunit.xml.dist # PHPUnit configuration
|
├── phpunit.xml.dist # PHPUnit configuration
|
||||||
└── wc-composable-product.php # Main plugin file
|
└── wc-composable-product.php # Main plugin file
|
||||||
```
|
```
|
||||||
@@ -140,11 +142,15 @@ Compile .po to .mo: `for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"
|
|||||||
|
|
||||||
WordPress requires compiled .mo files — .po files alone are insufficient.
|
WordPress requires compiled .mo files — .po files alone are insufficient.
|
||||||
|
|
||||||
## Testing
|
## Testing & Linting
|
||||||
|
|
||||||
Run unit tests: `vendor/bin/phpunit --testdox`
|
Run unit tests: `vendor/bin/phpunit --testdox`
|
||||||
|
|
||||||
Tests use **Brain Monkey** to mock WordPress/WooCommerce functions without a full WP installation. WooCommerce classes (`WC_Product`, `WC_Settings_Page`, etc.) are provided as minimal stubs in `tests/stubs/` so PHP can parse `extends` declarations. The release workflow runs tests before building — a failing test blocks the release.
|
Run coding standards check: `vendor/bin/phpcs`
|
||||||
|
|
||||||
|
Auto-fix coding standard violations: `vendor/bin/phpcbf`
|
||||||
|
|
||||||
|
Tests use **Brain Monkey** to mock WordPress/WooCommerce functions without a full WP installation. WooCommerce classes (`WC_Product`, `WC_Settings_Page`, etc.) are provided as minimal stubs in `tests/stubs/` so PHP can parse `extends` declarations. PHPCS uses the **WordPress-Extra** standard plus **PHPCompatibilityWP** for PHP version checks. The release workflow runs lint, phpcs, and tests before building — any failure blocks the release.
|
||||||
|
|
||||||
## Release Workflow
|
## Release Workflow
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,12 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"brain/monkey": "^2.7",
|
"brain/monkey": "^2.7",
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"phpunit/phpunit": "^9.6"
|
"phpcompatibility/phpcompatibility-wp": "*",
|
||||||
|
"phpunit/phpunit": "^10.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7",
|
||||||
|
"wp-coding-standards/wpcs": "^3.0"
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -30,6 +34,12 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"optimize-autoloader": true,
|
"optimize-autoloader": true,
|
||||||
"sort-packages": true
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"php": "8.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1161
composer.lock
generated
1161
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,10 @@ class ProductData {
|
|||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
add_filter('woocommerce_product_data_tabs', [$this, 'add_product_data_tab']);
|
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_data_tab' ) );
|
||||||
add_action('woocommerce_product_data_panels', [$this, 'add_product_data_panel']);
|
add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_data_panel' ) );
|
||||||
add_action('woocommerce_process_product_meta_composable', [$this, 'save_product_data']);
|
add_action( 'woocommerce_process_product_meta_composable', array( $this, 'save_product_data' ) );
|
||||||
add_action('woocommerce_product_options_general_product_data', [$this, 'add_general_fields']);
|
add_action( 'woocommerce_product_options_general_product_data', array( $this, 'add_general_fields' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,12 +30,12 @@ class ProductData {
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function add_product_data_tab( $tabs ) {
|
public function add_product_data_tab( $tabs ) {
|
||||||
$tabs['composable'] = [
|
$tabs['composable'] = array(
|
||||||
'label' => __( 'Composable Options', 'wc-composable-product' ),
|
'label' => __( 'Composable Options', 'wc-composable-product' ),
|
||||||
'target' => 'composable_product_data',
|
'target' => 'composable_product_data',
|
||||||
'class' => ['show_if_composable'],
|
'class' => array( 'show_if_composable' ),
|
||||||
'priority' => 21,
|
'priority' => 21,
|
||||||
];
|
);
|
||||||
return $tabs;
|
return $tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,31 +48,36 @@ class ProductData {
|
|||||||
if ( $product_object && $product_object->get_type() === 'composable' ) {
|
if ( $product_object && $product_object->get_type() === 'composable' ) {
|
||||||
echo '<div class="options_group show_if_composable">';
|
echo '<div class="options_group show_if_composable">';
|
||||||
|
|
||||||
woocommerce_wp_text_input([
|
woocommerce_wp_text_input(
|
||||||
|
array(
|
||||||
'id' => '_composable_selection_limit',
|
'id' => '_composable_selection_limit',
|
||||||
'label' => __( 'Selection Limit', 'wc-composable-product' ),
|
'label' => __( 'Selection Limit', 'wc-composable-product' ),
|
||||||
'description' => __( 'Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product' ),
|
'description' => __( 'Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product' ),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'custom_attributes' => [
|
'custom_attributes' => array(
|
||||||
'min' => '1',
|
'min' => '1',
|
||||||
'step' => '1',
|
'step' => '1',
|
||||||
],
|
),
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
woocommerce_wp_select([
|
woocommerce_wp_select(
|
||||||
|
array(
|
||||||
'id' => '_composable_pricing_mode',
|
'id' => '_composable_pricing_mode',
|
||||||
'label' => __( 'Pricing Mode', 'wc-composable-product' ),
|
'label' => __( 'Pricing Mode', 'wc-composable-product' ),
|
||||||
'description' => __( 'How to calculate the price.', 'wc-composable-product' ),
|
'description' => __( 'How to calculate the price.', 'wc-composable-product' ),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
'options' => [
|
'options' => array(
|
||||||
'' => __( 'Use global default', 'wc-composable-product' ),
|
'' => __( 'Use global default', 'wc-composable-product' ),
|
||||||
'sum' => __( 'Sum of selected products', 'wc-composable-product' ),
|
'sum' => __( 'Sum of selected products', 'wc-composable-product' ),
|
||||||
'fixed' => __( 'Fixed price', 'wc-composable-product' ),
|
'fixed' => __( 'Fixed price', 'wc-composable-product' ),
|
||||||
],
|
),
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
woocommerce_wp_text_input([
|
woocommerce_wp_text_input(
|
||||||
|
array(
|
||||||
'id' => '_regular_price',
|
'id' => '_regular_price',
|
||||||
'label' => __( 'Fixed Price', 'wc-composable-product' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
'label' => __( 'Fixed Price', 'wc-composable-product' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||||
'description' => __( 'Enter the fixed price for this composable product.', 'wc-composable-product' ),
|
'description' => __( 'Enter the fixed price for this composable product.', 'wc-composable-product' ),
|
||||||
@@ -80,7 +85,8 @@ class ProductData {
|
|||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'data_type' => 'price',
|
'data_type' => 'price',
|
||||||
'wrapper_class' => 'composable_fixed_price_field',
|
'wrapper_class' => 'composable_fixed_price_field',
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
@@ -95,31 +101,35 @@ class ProductData {
|
|||||||
<div id="composable_product_data" class="panel woocommerce_options_panel hidden">
|
<div id="composable_product_data" class="panel woocommerce_options_panel hidden">
|
||||||
<div class="options_group">
|
<div class="options_group">
|
||||||
<?php
|
<?php
|
||||||
woocommerce_wp_select([
|
woocommerce_wp_select(
|
||||||
|
array(
|
||||||
'id' => '_composable_include_unpublished',
|
'id' => '_composable_include_unpublished',
|
||||||
'label' => __( 'Include Non-Public Products', 'wc-composable-product' ),
|
'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' ),
|
'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,
|
'desc_tip' => true,
|
||||||
'options' => [
|
'options' => array(
|
||||||
'' => __( 'Use global default', 'wc-composable-product' ),
|
'' => __( 'Use global default', 'wc-composable-product' ),
|
||||||
'yes' => __( 'Yes', 'wc-composable-product' ),
|
'yes' => __( 'Yes', 'wc-composable-product' ),
|
||||||
'no' => __( 'No', 'wc-composable-product' ),
|
'no' => __( 'No', 'wc-composable-product' ),
|
||||||
],
|
),
|
||||||
'value' => get_post_meta($post->ID, '_composable_include_unpublished', true) ?: '',
|
'value' => get_post_meta( $post->ID, '_composable_include_unpublished', true ) ? get_post_meta( $post->ID, '_composable_include_unpublished', true ) : '',
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
woocommerce_wp_select([
|
woocommerce_wp_select(
|
||||||
|
array(
|
||||||
'id' => '_composable_criteria_type',
|
'id' => '_composable_criteria_type',
|
||||||
'label' => __( 'Selection Criteria', 'wc-composable-product' ),
|
'label' => __( 'Selection Criteria', 'wc-composable-product' ),
|
||||||
'description' => __( 'How to select available products.', 'wc-composable-product' ),
|
'description' => __( 'How to select available products.', 'wc-composable-product' ),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
'options' => [
|
'options' => array(
|
||||||
'category' => __( 'By Category', 'wc-composable-product' ),
|
'category' => __( 'By Category', 'wc-composable-product' ),
|
||||||
'tag' => __( 'By Tag', 'wc-composable-product' ),
|
'tag' => __( 'By Tag', 'wc-composable-product' ),
|
||||||
'sku' => __( 'By SKU', 'wc-composable-product' ),
|
'sku' => __( 'By SKU', 'wc-composable-product' ),
|
||||||
],
|
),
|
||||||
'value' => get_post_meta($post->ID, '_composable_criteria_type', true) ?: 'category',
|
'value' => get_post_meta( $post->ID, '_composable_criteria_type', true ) ? get_post_meta( $post->ID, '_composable_criteria_type', true ) : 'category',
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -128,13 +138,19 @@ class ProductData {
|
|||||||
<label for="_composable_categories"><?php esc_html_e( 'Select Categories', 'wc-composable-product' ); ?></label>
|
<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%;">
|
<select id="_composable_categories" name="_composable_categories[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||||
<?php
|
<?php
|
||||||
$selected_categories = get_post_meta($post->ID, '_composable_categories', true) ?: [];
|
$selected_categories = get_post_meta( $post->ID, '_composable_categories', true );
|
||||||
$categories = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => false]);
|
$selected_categories = $selected_categories ? $selected_categories : array();
|
||||||
|
$categories = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => 'product_cat',
|
||||||
|
'hide_empty' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
foreach ( $categories as $category ) {
|
foreach ( $categories as $category ) {
|
||||||
printf(
|
printf(
|
||||||
'<option value="%s" %s>%s</option>',
|
'<option value="%s" %s>%s</option>',
|
||||||
esc_attr( $category->term_id ),
|
esc_attr( $category->term_id ),
|
||||||
selected(in_array($category->term_id, (array) $selected_categories), true, false),
|
selected( in_array( $category->term_id, (array) $selected_categories, true ), true, false ),
|
||||||
esc_html( $category->name )
|
esc_html( $category->name )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -149,13 +165,19 @@ class ProductData {
|
|||||||
<label for="_composable_tags"><?php esc_html_e( 'Select Tags', 'wc-composable-product' ); ?></label>
|
<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%;">
|
<select id="_composable_tags" name="_composable_tags[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||||
<?php
|
<?php
|
||||||
$selected_tags = get_post_meta($post->ID, '_composable_tags', true) ?: [];
|
$selected_tags = get_post_meta( $post->ID, '_composable_tags', true );
|
||||||
$tags = get_terms(['taxonomy' => 'product_tag', 'hide_empty' => false]);
|
$selected_tags = $selected_tags ? $selected_tags : array();
|
||||||
|
$tags = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => 'product_tag',
|
||||||
|
'hide_empty' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
foreach ( $tags as $tag ) {
|
foreach ( $tags as $tag ) {
|
||||||
printf(
|
printf(
|
||||||
'<option value="%s" %s>%s</option>',
|
'<option value="%s" %s>%s</option>',
|
||||||
esc_attr( $tag->term_id ),
|
esc_attr( $tag->term_id ),
|
||||||
selected(in_array($tag->term_id, (array) $selected_tags), true, false),
|
selected( in_array( $tag->term_id, (array) $selected_tags, true ), true, false ),
|
||||||
esc_html( $tag->name )
|
esc_html( $tag->name )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -167,13 +189,15 @@ class ProductData {
|
|||||||
|
|
||||||
<div class="options_group composable_criteria_group" id="composable_criteria_sku" style="display: none;">
|
<div class="options_group composable_criteria_group" id="composable_criteria_sku" style="display: none;">
|
||||||
<?php
|
<?php
|
||||||
woocommerce_wp_textarea_input([
|
woocommerce_wp_textarea_input(
|
||||||
|
array(
|
||||||
'id' => '_composable_skus',
|
'id' => '_composable_skus',
|
||||||
'label' => __( 'Product SKUs', 'wc-composable-product' ),
|
'label' => __( 'Product SKUs', 'wc-composable-product' ),
|
||||||
'description' => __( 'Enter product SKUs separated by commas.', 'wc-composable-product' ),
|
'description' => __( 'Enter product SKUs separated by commas.', 'wc-composable-product' ),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
'placeholder' => __( 'SKU-1, SKU-2, SKU-3', 'wc-composable-product' ),
|
'placeholder' => __( 'SKU-1, SKU-2, SKU-3', 'wc-composable-product' ),
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,32 +210,36 @@ class ProductData {
|
|||||||
* @param int $post_id Post ID
|
* @param int $post_id Post ID
|
||||||
*/
|
*/
|
||||||
public function save_product_data( $post_id ) {
|
public function save_product_data( $post_id ) {
|
||||||
// Save selection limit
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce in woocommerce_process_product_meta.
|
||||||
|
|
||||||
|
// Save selection limit.
|
||||||
$selection_limit = isset( $_POST['_composable_selection_limit'] ) ? absint( $_POST['_composable_selection_limit'] ) : '';
|
$selection_limit = isset( $_POST['_composable_selection_limit'] ) ? absint( $_POST['_composable_selection_limit'] ) : '';
|
||||||
update_post_meta( $post_id, '_composable_selection_limit', $selection_limit );
|
update_post_meta( $post_id, '_composable_selection_limit', $selection_limit );
|
||||||
|
|
||||||
// Save pricing mode
|
// Save pricing mode.
|
||||||
$pricing_mode = isset( $_POST['_composable_pricing_mode'] ) ? sanitize_text_field( $_POST['_composable_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 );
|
update_post_meta( $post_id, '_composable_pricing_mode', $pricing_mode );
|
||||||
|
|
||||||
// Save include unpublished
|
// Save include unpublished.
|
||||||
$include_unpublished = isset( $_POST['_composable_include_unpublished'] ) ? sanitize_text_field( $_POST['_composable_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 );
|
update_post_meta( $post_id, '_composable_include_unpublished', $include_unpublished );
|
||||||
|
|
||||||
// Save criteria type
|
// Save criteria type.
|
||||||
$criteria_type = isset( $_POST['_composable_criteria_type'] ) ? sanitize_text_field( $_POST['_composable_criteria_type'] ) : 'category';
|
$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 );
|
update_post_meta( $post_id, '_composable_criteria_type', $criteria_type );
|
||||||
|
|
||||||
// Save categories
|
// Save categories.
|
||||||
$categories = isset($_POST['_composable_categories']) ? array_map('absint', $_POST['_composable_categories']) : [];
|
$categories = isset( $_POST['_composable_categories'] ) ? array_map( 'absint', $_POST['_composable_categories'] ) : array();
|
||||||
update_post_meta( $post_id, '_composable_categories', $categories );
|
update_post_meta( $post_id, '_composable_categories', $categories );
|
||||||
|
|
||||||
// Save tags
|
// Save tags.
|
||||||
$tags = isset($_POST['_composable_tags']) ? array_map('absint', $_POST['_composable_tags']) : [];
|
$tags = isset( $_POST['_composable_tags'] ) ? array_map( 'absint', $_POST['_composable_tags'] ) : array();
|
||||||
update_post_meta( $post_id, '_composable_tags', $tags );
|
update_post_meta( $post_id, '_composable_tags', $tags );
|
||||||
|
|
||||||
// Save SKUs
|
// Save SKUs.
|
||||||
$skus = isset( $_POST['_composable_skus'] ) ? sanitize_textarea_field( $_POST['_composable_skus'] ) : '';
|
$skus = isset( $_POST['_composable_skus'] ) ? sanitize_textarea_field( $_POST['_composable_skus'] ) : '';
|
||||||
update_post_meta( $post_id, '_composable_skus', $skus );
|
update_post_meta( $post_id, '_composable_skus', $skus );
|
||||||
|
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,70 +29,70 @@ class Settings extends \WC_Settings_Page {
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function get_settings() {
|
public function get_settings() {
|
||||||
$settings = [
|
$settings = array(
|
||||||
[
|
array(
|
||||||
'title' => __( 'Composable Products Settings', 'wc-composable-product' ),
|
'title' => __( 'Composable Products Settings', 'wc-composable-product' ),
|
||||||
'type' => 'title',
|
'type' => 'title',
|
||||||
'desc' => __( 'Configure default settings for composable products.', 'wc-composable-product' ),
|
'desc' => __( 'Configure default settings for composable products.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_settings',
|
'id' => 'wc_composable_settings',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __( 'Default Selection Limit', 'wc-composable-product' ),
|
'title' => __( 'Default Selection Limit', 'wc-composable-product' ),
|
||||||
'desc' => __( 'Default number of items customers can select.', 'wc-composable-product' ),
|
'desc' => __( 'Default number of items customers can select.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_default_limit',
|
'id' => 'wc_composable_default_limit',
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'default' => '5',
|
'default' => '5',
|
||||||
'custom_attributes' => [
|
'custom_attributes' => array(
|
||||||
'min' => '1',
|
'min' => '1',
|
||||||
'step' => '1',
|
'step' => '1',
|
||||||
],
|
),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __( 'Default Pricing Mode', 'wc-composable-product' ),
|
'title' => __( 'Default Pricing Mode', 'wc-composable-product' ),
|
||||||
'desc' => __( 'How to calculate the price of composable products.', 'wc-composable-product' ),
|
'desc' => __( 'How to calculate the price of composable products.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_default_pricing',
|
'id' => 'wc_composable_default_pricing',
|
||||||
'type' => 'select',
|
'type' => 'select',
|
||||||
'default' => 'sum',
|
'default' => 'sum',
|
||||||
'options' => [
|
'options' => array(
|
||||||
'sum' => __( 'Sum of selected products', 'wc-composable-product' ),
|
'sum' => __( 'Sum of selected products', 'wc-composable-product' ),
|
||||||
'fixed' => __( 'Fixed price', 'wc-composable-product' ),
|
'fixed' => __( 'Fixed price', 'wc-composable-product' ),
|
||||||
],
|
),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __( 'Include Non-Public Products', 'wc-composable-product' ),
|
'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' ),
|
'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',
|
'id' => 'wc_composable_include_unpublished',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'default' => 'no',
|
'default' => 'no',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __( 'Show Product Images', 'wc-composable-product' ),
|
'title' => __( 'Show Product Images', 'wc-composable-product' ),
|
||||||
'desc' => __( 'Display product images in the selection interface.', 'wc-composable-product' ),
|
'desc' => __( 'Display product images in the selection interface.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_show_images',
|
'id' => 'wc_composable_show_images',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'default' => 'yes',
|
'default' => 'yes',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __( 'Show Product Prices', 'wc-composable-product' ),
|
'title' => __( 'Show Product Prices', 'wc-composable-product' ),
|
||||||
'desc' => __( 'Display individual product prices in the selection interface.', 'wc-composable-product' ),
|
'desc' => __( 'Display individual product prices in the selection interface.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_show_prices',
|
'id' => 'wc_composable_show_prices',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'default' => 'yes',
|
'default' => 'yes',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __( 'Show Total Price', 'wc-composable-product' ),
|
'title' => __( 'Show Total Price', 'wc-composable-product' ),
|
||||||
'desc' => __( 'Display the total price as customers make selections.', 'wc-composable-product' ),
|
'desc' => __( 'Display the total price as customers make selections.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_show_total',
|
'id' => 'wc_composable_show_total',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'default' => 'yes',
|
'default' => 'yes',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'type' => 'sectionend',
|
'type' => 'sectionend',
|
||||||
'id' => 'wc_composable_settings',
|
'id' => 'wc_composable_settings',
|
||||||
],
|
),
|
||||||
];
|
);
|
||||||
|
|
||||||
return apply_filters( 'wc_composable_settings', $settings );
|
return apply_filters( 'wc_composable_settings', $settings );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ class CartHandler {
|
|||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->stock_manager = new StockManager();
|
$this->stock_manager = new StockManager();
|
||||||
|
|
||||||
add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3);
|
add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'validate_add_to_cart' ), 10, 3 );
|
||||||
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 2);
|
add_filter( 'woocommerce_add_cart_item_data', array( $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_cart_item_from_session', array( $this, 'get_cart_item_from_session' ), 10, 2 );
|
||||||
add_filter('woocommerce_get_item_data', [$this, 'display_cart_item_data'], 10, 2);
|
add_filter( 'woocommerce_get_item_data', array( $this, 'display_cart_item_data' ), 10, 2 );
|
||||||
add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']);
|
add_action( 'woocommerce_before_calculate_totals', array( $this, 'calculate_cart_item_price' ) );
|
||||||
add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25);
|
add_action( 'woocommerce_single_product_summary', array( $this, 'render_product_selector' ), 25 );
|
||||||
add_action('woocommerce_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3);
|
add_action( 'woocommerce_checkout_create_order_line_item', array( $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_is_purchasable', array( $this, 'hide_default_add_to_cart' ), 10, 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,12 +78,14 @@ class CartHandler {
|
|||||||
return $passed;
|
return $passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if selected products are provided
|
// 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'] ) ) {
|
if ( ! isset( $_POST['composable_products'] ) || empty( $_POST['composable_products'] ) ) {
|
||||||
wc_add_notice( __( 'Please select at least one product.', 'wc-composable-product' ), 'error' );
|
wc_add_notice( __( 'Please select at least one product.', 'wc-composable-product' ), 'error' );
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
||||||
$selected_products = array_map( 'absint', $_POST['composable_products'] );
|
$selected_products = array_map( 'absint', $_POST['composable_products'] );
|
||||||
$selection_limit = $product->get_selection_limit();
|
$selection_limit = $product->get_selection_limit();
|
||||||
|
|
||||||
@@ -101,12 +103,15 @@ class CartHandler {
|
|||||||
|
|
||||||
// Validate that selected products are valid
|
// Validate that selected products are valid
|
||||||
$available_products = $product->get_available_products();
|
$available_products = $product->get_available_products();
|
||||||
$available_ids = array_map(function($p) {
|
$available_ids = array_map(
|
||||||
|
function ( $p ) {
|
||||||
return $p->get_id();
|
return $p->get_id();
|
||||||
}, $available_products);
|
},
|
||||||
|
$available_products
|
||||||
|
);
|
||||||
|
|
||||||
foreach ( $selected_products as $selected_id ) {
|
foreach ( $selected_products as $selected_id ) {
|
||||||
if (!in_array($selected_id, $available_ids)) {
|
if ( ! in_array( $selected_id, $available_ids, true ) ) {
|
||||||
wc_add_notice( __( 'One or more selected products are not available.', 'wc-composable-product' ), 'error' );
|
wc_add_notice( __( 'One or more selected products are not available.', 'wc-composable-product' ), 'error' );
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -114,7 +119,7 @@ class CartHandler {
|
|||||||
|
|
||||||
// Validate stock availability
|
// Validate stock availability
|
||||||
$stock_validation = $this->stock_manager->validate_stock_availability( $selected_products, $quantity );
|
$stock_validation = $this->stock_manager->validate_stock_availability( $selected_products, $quantity );
|
||||||
if ($stock_validation !== true) {
|
if ( true !== $stock_validation ) {
|
||||||
wc_add_notice( $stock_validation, 'error' );
|
wc_add_notice( $stock_validation, 'error' );
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -136,12 +141,14 @@ class CartHandler {
|
|||||||
return $cart_item_data;
|
return $cart_item_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
||||||
if ( isset( $_POST['composable_products'] ) && ! empty( $_POST['composable_products'] ) ) {
|
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'] );
|
$selected_products = array_map( 'absint', $_POST['composable_products'] );
|
||||||
$cart_item_data['composable_products'] = $selected_products;
|
$cart_item_data['composable_products'] = $selected_products;
|
||||||
|
|
||||||
// Make cart item unique
|
// Make cart item unique.
|
||||||
$cart_item_data['unique_key'] = md5(json_encode($selected_products) . time());
|
$cart_item_data['unique_key'] = md5( wp_json_encode( $selected_products ) . time() );
|
||||||
}
|
}
|
||||||
|
|
||||||
return $cart_item_data;
|
return $cart_item_data;
|
||||||
@@ -171,7 +178,7 @@ class CartHandler {
|
|||||||
*/
|
*/
|
||||||
public function display_cart_item_data( $item_data, $cart_item ) {
|
public function display_cart_item_data( $item_data, $cart_item ) {
|
||||||
if ( isset( $cart_item['composable_products'] ) && ! empty( $cart_item['composable_products'] ) ) {
|
if ( isset( $cart_item['composable_products'] ) && ! empty( $cart_item['composable_products'] ) ) {
|
||||||
$product_names = [];
|
$product_names = array();
|
||||||
foreach ( $cart_item['composable_products'] as $product_id ) {
|
foreach ( $cart_item['composable_products'] as $product_id ) {
|
||||||
$product = wc_get_product( $product_id );
|
$product = wc_get_product( $product_id );
|
||||||
if ( $product ) {
|
if ( $product ) {
|
||||||
@@ -180,10 +187,10 @@ class CartHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( ! empty( $product_names ) ) {
|
if ( ! empty( $product_names ) ) {
|
||||||
$item_data[] = [
|
$item_data[] = array(
|
||||||
'key' => __( 'Selected Products', 'wc-composable-product' ),
|
'key' => __( 'Selected Products', 'wc-composable-product' ),
|
||||||
'value' => implode( ', ', $product_names ),
|
'value' => implode( ', ', $product_names ),
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,15 +55,15 @@ class Plugin {
|
|||||||
*/
|
*/
|
||||||
private function init_hooks() {
|
private function init_hooks() {
|
||||||
// Register product type
|
// Register product type
|
||||||
add_filter('product_type_selector', [$this, 'add_product_type']);
|
add_filter( 'product_type_selector', array( $this, 'add_product_type' ) );
|
||||||
add_filter('woocommerce_product_class', [$this, 'product_class'], 10, 2);
|
add_filter( 'woocommerce_product_class', array( $this, 'product_class' ), 10, 2 );
|
||||||
|
|
||||||
// Enqueue scripts and styles
|
// Enqueue scripts and styles
|
||||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_scripts']);
|
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
|
||||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
|
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
|
||||||
|
|
||||||
// Admin settings
|
// Admin settings
|
||||||
add_filter('woocommerce_get_settings_pages', [$this, 'add_settings_page']);
|
add_filter( 'woocommerce_get_settings_pages', array( $this, 'add_settings_page' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,16 +71,25 @@ class Plugin {
|
|||||||
*/
|
*/
|
||||||
private function init_twig() {
|
private function init_twig() {
|
||||||
$loader = new \Twig\Loader\FilesystemLoader( WC_COMPOSABLE_PRODUCT_PATH . 'templates' );
|
$loader = new \Twig\Loader\FilesystemLoader( WC_COMPOSABLE_PRODUCT_PATH . 'templates' );
|
||||||
$this->twig = new \Twig\Environment($loader, [
|
$this->twig = new \Twig\Environment(
|
||||||
|
$loader,
|
||||||
|
array(
|
||||||
'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache',
|
'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache',
|
||||||
'auto_reload' => true,
|
'auto_reload' => true,
|
||||||
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
|
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Add WordPress functions to Twig
|
// Add WordPress functions to Twig
|
||||||
$this->twig->addFunction(new \Twig\TwigFunction('__', function($text) {
|
$this->twig->addFunction(
|
||||||
|
new \Twig\TwigFunction(
|
||||||
|
'__',
|
||||||
|
function ( $text ) {
|
||||||
|
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- dynamic Twig template strings.
|
||||||
return __( $text, 'wc-composable-product' );
|
return __( $text, 'wc-composable-product' );
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_html', 'esc_html' ) );
|
$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_attr', 'esc_attr' ) );
|
||||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_url', 'esc_url' ) );
|
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_url', 'esc_url' ) );
|
||||||
@@ -128,7 +137,7 @@ class Plugin {
|
|||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function product_class( $classname, $product_type ) {
|
public function product_class( $classname, $product_type ) {
|
||||||
if ($product_type === 'composable') {
|
if ( 'composable' === $product_type ) {
|
||||||
$classname = 'Magdev\WcComposableProduct\ProductType';
|
$classname = 'Magdev\WcComposableProduct\ProductType';
|
||||||
}
|
}
|
||||||
return $classname;
|
return $classname;
|
||||||
@@ -142,34 +151,38 @@ class Plugin {
|
|||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'wc-composable-product',
|
'wc-composable-product',
|
||||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css',
|
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css',
|
||||||
[],
|
array(),
|
||||||
WC_COMPOSABLE_PRODUCT_VERSION
|
WC_COMPOSABLE_PRODUCT_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'wc-composable-product',
|
'wc-composable-product',
|
||||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js',
|
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js',
|
||||||
['jquery'],
|
array( 'jquery' ),
|
||||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_localize_script('wc-composable-product', 'wcComposableProduct', [
|
wp_localize_script(
|
||||||
|
'wc-composable-product',
|
||||||
|
'wcComposableProduct',
|
||||||
|
array(
|
||||||
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||||
'nonce' => wp_create_nonce( 'wc_composable_product_nonce' ),
|
'nonce' => wp_create_nonce( 'wc_composable_product_nonce' ),
|
||||||
'i18n' => [
|
'i18n' => array(
|
||||||
'select_items' => __( 'Please select items', 'wc-composable-product' ),
|
'select_items' => __( 'Please select items', 'wc-composable-product' ),
|
||||||
'max_items' => __( 'Maximum items selected', 'wc-composable-product' ),
|
'max_items' => __( 'Maximum items selected', 'wc-composable-product' ),
|
||||||
'min_items' => __( 'Please select at least one item', 'wc-composable-product' ),
|
'min_items' => __( 'Please select at least one item', 'wc-composable-product' ),
|
||||||
],
|
),
|
||||||
'price_format' => [
|
'price_format' => array(
|
||||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||||
'decimal_separator' => wc_get_price_decimal_separator(),
|
'decimal_separator' => wc_get_price_decimal_separator(),
|
||||||
'thousand_separator' => wc_get_price_thousand_separator(),
|
'thousand_separator' => wc_get_price_thousand_separator(),
|
||||||
'decimals' => wc_get_price_decimals(),
|
'decimals' => wc_get_price_decimals(),
|
||||||
'price_format' => get_woocommerce_price_format(),
|
'price_format' => get_woocommerce_price_format(),
|
||||||
],
|
),
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,14 +196,14 @@ class Plugin {
|
|||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'wc-composable-product-admin',
|
'wc-composable-product-admin',
|
||||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css',
|
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css',
|
||||||
[],
|
array(),
|
||||||
WC_COMPOSABLE_PRODUCT_VERSION
|
WC_COMPOSABLE_PRODUCT_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'wc-composable-product-admin',
|
'wc-composable-product-admin',
|
||||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js',
|
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js',
|
||||||
['jquery', 'wc-admin-product-meta-boxes'],
|
array( 'jquery', 'wc-admin-product-meta-boxes' ),
|
||||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -227,7 +240,7 @@ class Plugin {
|
|||||||
* @param array $context Template variables
|
* @param array $context Template variables
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function render_template($template, $context = []) {
|
public function render_template( $template, $context = array() ) {
|
||||||
return $this->twig->render( $template, $context );
|
return $this->twig->render( $template, $context );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ class ProductSelector {
|
|||||||
$stock_manager = new StockManager();
|
$stock_manager = new StockManager();
|
||||||
|
|
||||||
// Prepare product data for template
|
// Prepare product data for template
|
||||||
$products_data = [];
|
$products_data = array();
|
||||||
foreach ( $available_products as $available_product ) {
|
foreach ( $available_products as $available_product ) {
|
||||||
$stock_info = $stock_manager->get_product_stock_info( $available_product->get_id() );
|
$stock_info = $stock_manager->get_product_stock_info( $available_product->get_id() );
|
||||||
|
|
||||||
$products_data[] = [
|
$products_data[] = array(
|
||||||
'id' => $available_product->get_id(),
|
'id' => $available_product->get_id(),
|
||||||
'name' => $available_product->get_name(),
|
'name' => $available_product->get_name(),
|
||||||
'price' => $available_product->get_price(),
|
'price' => $available_product->get_price(),
|
||||||
@@ -53,10 +53,10 @@ class ProductSelector {
|
|||||||
'stock_quantity' => $stock_info['stock_quantity'],
|
'stock_quantity' => $stock_info['stock_quantity'],
|
||||||
'managing_stock' => $stock_info['managing_stock'],
|
'managing_stock' => $stock_info['managing_stock'],
|
||||||
'backorders_allowed' => $stock_info['backorders_allowed'],
|
'backorders_allowed' => $stock_info['backorders_allowed'],
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$context = [
|
$context = array(
|
||||||
'product_id' => $product->get_id(),
|
'product_id' => $product->get_id(),
|
||||||
'products' => $products_data,
|
'products' => $products_data,
|
||||||
'selection_limit' => $selection_limit,
|
'selection_limit' => $selection_limit,
|
||||||
@@ -68,10 +68,11 @@ class ProductSelector {
|
|||||||
'fixed_price_html' => wc_price( $product->get_price() ),
|
'fixed_price_html' => wc_price( $product->get_price() ),
|
||||||
'zero_price_html' => wc_price( 0 ),
|
'zero_price_html' => wc_price( 0 ),
|
||||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||||
];
|
);
|
||||||
|
|
||||||
// Render template
|
// Render template — Twig handles escaping via registered esc_html/esc_attr/esc_url functions.
|
||||||
$plugin = Plugin::instance();
|
$plugin = Plugin::instance();
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped by Twig template.
|
||||||
echo $plugin->render_template( 'product-selector.twig', $context );
|
echo $plugin->render_template( 'product-selector.twig', $context );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,12 +71,17 @@ class ProductType extends \WC_Product {
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function get_selection_criteria() {
|
public function get_selection_criteria() {
|
||||||
return [
|
$type = $this->get_meta( '_composable_criteria_type', true );
|
||||||
'type' => $this->get_meta('_composable_criteria_type', true) ?: 'category',
|
$categories = $this->get_meta( '_composable_categories', true );
|
||||||
'categories' => $this->get_meta('_composable_categories', true) ?: [],
|
$tags = $this->get_meta( '_composable_tags', true );
|
||||||
'tags' => $this->get_meta('_composable_tags', true) ?: [],
|
$skus = $this->get_meta( '_composable_skus', true );
|
||||||
'skus' => $this->get_meta('_composable_skus', true) ?: '',
|
|
||||||
];
|
return array(
|
||||||
|
'type' => $type ? $type : 'category',
|
||||||
|
'categories' => $categories ? $categories : array(),
|
||||||
|
'tags' => $tags ? $tags : array(),
|
||||||
|
'skus' => $skus ? $skus : '',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,13 +109,13 @@ class ProductType extends \WC_Product {
|
|||||||
*/
|
*/
|
||||||
public function should_include_unpublished() {
|
public function should_include_unpublished() {
|
||||||
$per_product = $this->get_meta( '_composable_include_unpublished', true );
|
$per_product = $this->get_meta( '_composable_include_unpublished', true );
|
||||||
if ($per_product === 'yes') {
|
if ( 'yes' === $per_product ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ($per_product === 'no') {
|
if ( 'no' === $per_product ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return get_option('wc_composable_include_unpublished', 'no') === 'yes';
|
return 'yes' === get_option( 'wc_composable_include_unpublished', 'no' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,65 +126,65 @@ class ProductType extends \WC_Product {
|
|||||||
public function get_available_products() {
|
public function get_available_products() {
|
||||||
$criteria = $this->get_selection_criteria();
|
$criteria = $this->get_selection_criteria();
|
||||||
$include_unpublished = $this->should_include_unpublished();
|
$include_unpublished = $this->should_include_unpublished();
|
||||||
$args = [
|
$args = array(
|
||||||
'post_type' => 'product',
|
'post_type' => 'product',
|
||||||
'posts_per_page' => -1,
|
'posts_per_page' => -1,
|
||||||
'post_status' => $include_unpublished ? ['publish', 'draft', 'private'] : 'publish',
|
'post_status' => $include_unpublished ? array( 'publish', 'draft', 'private' ) : 'publish',
|
||||||
'orderby' => 'title',
|
'orderby' => 'title',
|
||||||
'order' => 'ASC',
|
'order' => 'ASC',
|
||||||
];
|
);
|
||||||
|
|
||||||
// Exclude composable products using the product_type taxonomy
|
// Exclude composable products using the product_type taxonomy
|
||||||
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
||||||
$args['tax_query'] = [
|
$args['tax_query'] = array(
|
||||||
'relation' => 'AND',
|
'relation' => 'AND',
|
||||||
[
|
array(
|
||||||
'taxonomy' => 'product_type',
|
'taxonomy' => 'product_type',
|
||||||
'field' => 'slug',
|
'field' => 'slug',
|
||||||
'terms' => ['composable'],
|
'terms' => array( 'composable' ),
|
||||||
'operator' => 'NOT IN',
|
'operator' => 'NOT IN',
|
||||||
],
|
),
|
||||||
];
|
);
|
||||||
|
|
||||||
switch ( $criteria['type'] ) {
|
switch ( $criteria['type'] ) {
|
||||||
case 'category':
|
case 'category':
|
||||||
if ( ! empty( $criteria['categories'] ) ) {
|
if ( ! empty( $criteria['categories'] ) ) {
|
||||||
$args['tax_query'][] = [
|
$args['tax_query'][] = array(
|
||||||
'taxonomy' => 'product_cat',
|
'taxonomy' => 'product_cat',
|
||||||
'field' => 'term_id',
|
'field' => 'term_id',
|
||||||
'terms' => $criteria['categories'],
|
'terms' => $criteria['categories'],
|
||||||
'operator' => 'IN',
|
'operator' => 'IN',
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tag':
|
case 'tag':
|
||||||
if ( ! empty( $criteria['tags'] ) ) {
|
if ( ! empty( $criteria['tags'] ) ) {
|
||||||
$args['tax_query'][] = [
|
$args['tax_query'][] = array(
|
||||||
'taxonomy' => 'product_tag',
|
'taxonomy' => 'product_tag',
|
||||||
'field' => 'term_id',
|
'field' => 'term_id',
|
||||||
'terms' => $criteria['tags'],
|
'terms' => $criteria['tags'],
|
||||||
'operator' => 'IN',
|
'operator' => 'IN',
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'sku':
|
case 'sku':
|
||||||
if ( ! empty( $criteria['skus'] ) ) {
|
if ( ! empty( $criteria['skus'] ) ) {
|
||||||
$skus = array_map( 'trim', explode( ',', $criteria['skus'] ) );
|
$skus = array_map( 'trim', explode( ',', $criteria['skus'] ) );
|
||||||
$args['meta_query'] = [
|
$args['meta_query'] = array(
|
||||||
[
|
array(
|
||||||
'key' => '_sku',
|
'key' => '_sku',
|
||||||
'value' => $skus,
|
'value' => $skus,
|
||||||
'compare' => 'IN',
|
'compare' => 'IN',
|
||||||
],
|
),
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = new \WP_Query( $args );
|
$query = new \WP_Query( $args );
|
||||||
$products = [];
|
$products = array();
|
||||||
|
|
||||||
if ( $query->have_posts() ) {
|
if ( $query->have_posts() ) {
|
||||||
foreach ( $query->posts as $post ) {
|
foreach ( $query->posts as $post ) {
|
||||||
@@ -218,7 +223,7 @@ class ProductType extends \WC_Product {
|
|||||||
public function calculate_composed_price( $selected_products ) {
|
public function calculate_composed_price( $selected_products ) {
|
||||||
$pricing_mode = $this->get_pricing_mode();
|
$pricing_mode = $this->get_pricing_mode();
|
||||||
|
|
||||||
if ($pricing_mode === 'fixed') {
|
if ( 'fixed' === $pricing_mode ) {
|
||||||
return floatval( $this->get_regular_price() );
|
return floatval( $this->get_regular_price() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +248,7 @@ class ProductType extends \WC_Product {
|
|||||||
* @param array $cart_item_data Cart item data
|
* @param array $cart_item_data Cart item data
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function add_to_cart_validation($product_id, $quantity, $variation_id = 0, $variations = [], $cart_item_data = []) {
|
public function add_to_cart_validation( $product_id, $quantity, $variation_id = 0, $variations = array(), $cart_item_data = array() ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ class StockManager {
|
|||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
// Hook into order completion to reduce stock
|
// 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_completed', array( $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_processing', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 );
|
||||||
|
|
||||||
// Hook into order cancellation/refund to restore stock
|
// 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_cancelled', array( $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_refunded', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 );
|
||||||
|
|
||||||
// Prevent double stock reduction
|
// 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 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +63,7 @@ class StockManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if enough stock is available
|
// Check if enough stock is available
|
||||||
if ($stock_quantity !== null && $stock_quantity < $quantity) {
|
if ( null !== $stock_quantity && $stock_quantity < $quantity ) {
|
||||||
return sprintf(
|
return sprintf(
|
||||||
/* translators: 1: product name, 2: stock quantity */
|
/* 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' ),
|
||||||
@@ -92,25 +92,25 @@ class StockManager {
|
|||||||
$product = wc_get_product( $product_id );
|
$product = wc_get_product( $product_id );
|
||||||
|
|
||||||
if ( ! $product ) {
|
if ( ! $product ) {
|
||||||
return [
|
return array(
|
||||||
'in_stock' => false,
|
'in_stock' => false,
|
||||||
'stock_quantity' => 0,
|
'stock_quantity' => 0,
|
||||||
'backorders_allowed' => false,
|
'backorders_allowed' => false,
|
||||||
'stock_status' => 'outofstock',
|
'stock_status' => 'outofstock',
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stock_quantity = $product->get_stock_quantity();
|
$stock_quantity = $product->get_stock_quantity();
|
||||||
$managing_stock = $product->managing_stock();
|
$managing_stock = $product->managing_stock();
|
||||||
|
|
||||||
return [
|
return array(
|
||||||
'in_stock' => $product->is_in_stock(),
|
'in_stock' => $product->is_in_stock(),
|
||||||
'stock_quantity' => $stock_quantity,
|
'stock_quantity' => $stock_quantity,
|
||||||
'backorders_allowed' => $product->backorders_allowed(),
|
'backorders_allowed' => $product->backorders_allowed(),
|
||||||
'stock_status' => $product->get_stock_status(),
|
'stock_status' => $product->get_stock_status(),
|
||||||
'managing_stock' => $managing_stock,
|
'managing_stock' => $managing_stock,
|
||||||
'has_enough_stock' => !$managing_stock || $stock_quantity === null || $stock_quantity >= $required_quantity,
|
'has_enough_stock' => ! $managing_stock || null === $stock_quantity || $stock_quantity >= $required_quantity,
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,7 +156,7 @@ class StockManager {
|
|||||||
|
|
||||||
$stock_quantity = $selected_product->get_stock_quantity();
|
$stock_quantity = $selected_product->get_stock_quantity();
|
||||||
|
|
||||||
if ($stock_quantity !== null) {
|
if ( null !== $stock_quantity ) {
|
||||||
$new_stock = $stock_quantity - $quantity;
|
$new_stock = $stock_quantity - $quantity;
|
||||||
$selected_product->set_stock_quantity( $new_stock );
|
$selected_product->set_stock_quantity( $new_stock );
|
||||||
$selected_product->save();
|
$selected_product->save();
|
||||||
@@ -223,7 +223,7 @@ class StockManager {
|
|||||||
|
|
||||||
$stock_quantity = $selected_product->get_stock_quantity();
|
$stock_quantity = $selected_product->get_stock_quantity();
|
||||||
|
|
||||||
if ($stock_quantity !== null) {
|
if ( null !== $stock_quantity ) {
|
||||||
$new_stock = $stock_quantity + $quantity;
|
$new_stock = $stock_quantity + $quantity;
|
||||||
$selected_product->set_stock_quantity( $new_stock );
|
$selected_product->set_stock_quantity( $new_stock );
|
||||||
$selected_product->save();
|
$selected_product->save();
|
||||||
|
|||||||
34
phpcs.xml.dist
Normal file
34
phpcs.xml.dist
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<ruleset name="WC Composable Product">
|
||||||
|
<description>PHPCS rules for WooCommerce Composable Products plugin</description>
|
||||||
|
|
||||||
|
<!-- Scan plugin source -->
|
||||||
|
<file>includes</file>
|
||||||
|
<file>wc-composable-product.php</file>
|
||||||
|
|
||||||
|
<!-- Show progress and use colors -->
|
||||||
|
<arg value="ps"/>
|
||||||
|
<arg name="colors"/>
|
||||||
|
|
||||||
|
<!-- Use WordPress Extra standard (Core + Extra, without Docs) -->
|
||||||
|
<rule ref="WordPress-Extra">
|
||||||
|
<!-- Allow PSR-4 PascalCase file naming -->
|
||||||
|
<exclude name="WordPress.Files.FileName"/>
|
||||||
|
</rule>
|
||||||
|
|
||||||
|
<!-- Check PHP 8.3+ compatibility -->
|
||||||
|
<rule ref="PHPCompatibilityWP"/>
|
||||||
|
<config name="testVersion" value="8.3-"/>
|
||||||
|
|
||||||
|
<!-- WordPress minimum version -->
|
||||||
|
<config name="minimum_wp_version" value="6.0"/>
|
||||||
|
|
||||||
|
<!-- Enforce text domain -->
|
||||||
|
<rule ref="WordPress.WP.I18n">
|
||||||
|
<properties>
|
||||||
|
<property name="text_domain" type="array">
|
||||||
|
<element value="wc-composable-product"/>
|
||||||
|
</property>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
</ruleset>
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit
|
<phpunit
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
|
||||||
bootstrap="tests/bootstrap.php"
|
bootstrap="tests/bootstrap.php"
|
||||||
colors="true"
|
colors="true"
|
||||||
verbose="true"
|
|
||||||
beStrictAboutTestsThatDoNotTestAnything="true"
|
|
||||||
beStrictAboutOutputDuringTests="true"
|
|
||||||
failOnRisky="false"
|
|
||||||
failOnWarning="true"
|
failOnWarning="true"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
>
|
>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Unit">
|
<testsuite name="Unit">
|
||||||
@@ -16,9 +13,9 @@
|
|||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
<coverage processUncoveredFiles="true">
|
<source>
|
||||||
<include>
|
<include>
|
||||||
<directory suffix=".php">includes</directory>
|
<directory suffix=".php">includes</directory>
|
||||||
</include>
|
</include>
|
||||||
</coverage>
|
</source>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
@@ -36,13 +36,16 @@ if (file_exists(WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php')) {
|
|||||||
*/
|
*/
|
||||||
function wc_composable_product_check_woocommerce() {
|
function wc_composable_product_check_woocommerce() {
|
||||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||||
add_action('admin_notices', function() {
|
add_action(
|
||||||
|
'admin_notices',
|
||||||
|
function () {
|
||||||
?>
|
?>
|
||||||
<div class="notice notice-error">
|
<div class="notice notice-error">
|
||||||
<p><?php esc_html_e( 'WooCommerce Composable Products requires WooCommerce to be installed and active.', 'wc-composable-product' ); ?></p>
|
<p><?php esc_html_e( 'WooCommerce Composable Products requires WooCommerce to be installed and active.', 'wc-composable-product' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -68,11 +71,14 @@ add_action('woocommerce_init', 'wc_composable_product_init');
|
|||||||
/**
|
/**
|
||||||
* Declare HPOS compatibility
|
* Declare HPOS compatibility
|
||||||
*/
|
*/
|
||||||
add_action('before_woocommerce_init', function() {
|
add_action(
|
||||||
|
'before_woocommerce_init',
|
||||||
|
function () {
|
||||||
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
|
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
|
||||||
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activation hook
|
* Activation hook
|
||||||
|
|||||||
Reference in New Issue
Block a user