-
Notifications
You must be signed in to change notification settings - Fork 70
First-party Woo plugin detection for product customizations #4552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev/develop
Are you sure you want to change the base?
Changes from 7 commits
915150c
862a280
5525860
d21caaa
c74c639
2024016
a81fbc5
2f6deb4
26a84bc
2728423
ef283de
e70d623
ed5bbbe
0d96bca
35dce42
98123db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <?php | ||
| /** | ||
| * The modules Runtime Exception. | ||
| * | ||
| * @package WooCommerce\PayPalCommerce\Compat\Exception | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace WooCommerce\PayPalCommerce\Compat\Exception; | ||
|
|
||
| /** | ||
| * Thrown when an API method of a plugin doesn't exist although that plugin is active. | ||
| */ | ||
| class PluginApiChangedException extends \RuntimeException { | ||
|
|
||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| <?php | ||
| /** | ||
| * Detects third-party plugins relevant for compatibility checks. | ||
| * | ||
| * @package WooCommerce\PayPalCommerce\Compat\PluginDetector | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace WooCommerce\PayPalCommerce\Compat\PluginDetector; | ||
|
|
||
| /** | ||
| * @see self::scan() for the plugins list. | ||
| */ | ||
| class PluginDetector { | ||
|
|
||
| /** | ||
| * @return array<string, bool> List of plugins check was made for, | ||
| * boolean shows whether the plugin is active | ||
| */ | ||
| public function scan(): array { | ||
| return array( | ||
| 'woocommerce-subscriptions' => $this->is_woocommerce_subscriptions_active(), | ||
| 'woocommerce-gift-cards' => $this->is_woocommerce_gift_cards_active(), | ||
| 'woocommerce-product-bundles' => $this->is_woocommerce_product_bundles_active(), | ||
| 'woocommerce-product-addons' => $this->is_woocommerce_product_addons_active(), | ||
| 'woocommerce-min-max-quantities' => $this->is_woocommerce_min_max_quantities_active(), | ||
| 'woocommerce-composite-products' => $this->is_woocommerce_composite_products_active(), | ||
| 'woocommerce-shipping-per-product' => $this->is_woocommerce_shipping_per_product_active(), | ||
| 'woocommerce-deposits' => $this->is_woocommerce_deposits_active(), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idea: turn these strings into Reasons:
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, thanks! Done. |
||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether WooCommerce Subscriptions is active. | ||
| * | ||
| * @return bool | ||
| */ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Remove all the docblocks in this file. Reason: the return value is documented in code. And the function names are excellent, they do not need any explanation
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, this is nothing more than noise. Removed now. |
||
| private function is_woocommerce_subscriptions_active(): bool { | ||
| return class_exists( \WC_Subscriptions::class ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether WooCommerce Gift Cards is active. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function is_woocommerce_gift_cards_active(): bool { | ||
| return function_exists( 'WC_GC' ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether WooCommerce Product Bundles is active. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function is_woocommerce_product_bundles_active(): bool { | ||
| return class_exists( \WC_Bundles::class ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether WooCommerce Product Add-Ons is active. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function is_woocommerce_product_addons_active(): bool { | ||
| return class_exists( \WC_Product_Addons::class ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether WooCommerce Min/Max Quantities is active. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function is_woocommerce_min_max_quantities_active(): bool { | ||
| return class_exists( \WC_Min_Max_Quantities::class ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether WooCommerce Composite Products is active. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function is_woocommerce_composite_products_active(): bool { | ||
| return class_exists( \WC_Composite_Products::class ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether WooCommerce Per-Product Shipping is active. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function is_woocommerce_shipping_per_product_active(): bool { | ||
| return class_exists( \WC_Shipping_Per_Product_Init::class ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether WooCommerce Deposits is active. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function is_woocommerce_deposits_active(): bool { | ||
| return defined( 'WC_DEPOSITS_VERSION' ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| <?php | ||
| /** | ||
| * Detects whether a product has been customized by a first-party | ||
| * WooCommerce extension (product type change or plugin-specific meta). | ||
| * | ||
| * @package WooCommerce\PayPalCommerce\Compat\PluginDetector | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: apply code style rules to this file Reason: The project style expects spaces around operators and parentheses Sample: declare( strict_types = 1 );
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed, thanks |
||
|
|
||
| namespace WooCommerce\PayPalCommerce\Compat\PluginDetector; | ||
|
|
||
| use Psr\Log\LoggerInterface; | ||
| use WooCommerce\PayPalCommerce\Compat\Exception\PluginApiChangedException; | ||
|
|
||
| /** | ||
| * @see self::scan() for the plugins list. | ||
| */ | ||
| class ProductCustomizationDetector { | ||
|
|
||
| /** | ||
| * @var PluginDetector | ||
| */ | ||
| private PluginDetector $plugin_detector; | ||
|
|
||
| /** | ||
| * @var LoggerInterface | ||
| */ | ||
| private LoggerInterface $logger; | ||
|
|
||
| /** | ||
| * Cached result of $plugin_detector->scan(), since plugin activation | ||
| * cannot change within a request but scan() may be called once per product. | ||
| * | ||
| * @var array<string, bool>|null | ||
| */ | ||
|
Comment on lines
+24
to
+29
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: This (just want to point out that this is different/useful in contrast to the comments above) |
||
| private ?array $active_plugins = null; | ||
|
|
||
| /** | ||
| * @param PluginDetector $plugin_detector The plugin presence detector. | ||
| * @param LoggerInterface $logger The logger. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also suggest to remove these comments as they do not convey any info
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, they are useless. Removed. |
||
| */ | ||
| public function __construct( PluginDetector $plugin_detector, LoggerInterface $logger ) { | ||
| $this->plugin_detector = $plugin_detector; | ||
| $this->logger = $logger; | ||
| } | ||
|
|
||
| /** | ||
| * @param \WC_Product $product The product to check. | ||
| * @return array<string, bool> List of plugins check was made for, | ||
| * boolean shows whether the plugin has customized the product. | ||
| */ | ||
| public function scan( \WC_Product $product ): array { | ||
| if ( null === $this->active_plugins ) { | ||
| $this->active_plugins = $this->plugin_detector->scan(); | ||
| } | ||
|
|
||
| $checks = array( | ||
| 'woocommerce-subscriptions' => array( $this, 'is_customized_by_subscriptions' ), | ||
| 'woocommerce-gift-cards' => array( $this, 'is_customized_by_gift_cards' ), | ||
| 'woocommerce-product-bundles' => array( $this, 'is_customized_by_product_bundles' ), | ||
| 'woocommerce-product-addons' => array( $this, 'is_customized_by_product_addons' ), | ||
| 'woocommerce-min-max-quantities' => array( $this, 'is_customized_by_min_max_quantities' ), | ||
| 'woocommerce-composite-products' => array( $this, 'is_customized_by_composite_products' ), | ||
| 'woocommerce-shipping-per-product' => array( $this, 'is_customized_by_shipping_per_product' ), | ||
| 'woocommerce-deposits' => array( $this, 'is_customized_by_deposits' ), | ||
| ); | ||
|
|
||
| $result = array(); | ||
| foreach ( $checks as $plugin => $check ) { | ||
| if ( empty( $this->active_plugins[ $plugin ] ) ) { | ||
| $result[ $plugin ] = false; | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| // Only guards checks that call a plugin method (see assert_method_exists()). | ||
| // Checks that read meta directly (min/max quantities, per-product shipping) | ||
| // have no class/method to assert against, so they stay unprotected here. | ||
| $result[ $plugin ] = (bool) call_user_func( $check, $product ); | ||
| } catch ( PluginApiChangedException $exception ) { | ||
| $this->logger->warning( "Product customization check for \"{$plugin}\" failed: " . $exception->getMessage() ); | ||
| $result[ $plugin ] = false; | ||
| } | ||
| } | ||
|
|
||
| return $result; | ||
| } | ||
|
|
||
| /** | ||
| * Throws if the given plugin method does not exist, even though the | ||
| * plugin was detected as active. This points at the plugin having | ||
| * changed its API since this check was written. | ||
| * | ||
| * @param string $class The fully qualified class name. | ||
| * @param string $method The method name. | ||
| * @throws PluginApiChangedException If the class or method does not exist. | ||
| */ | ||
| private function assert_method_exists( string $class, string $method ): void { | ||
| if ( ! method_exists( $class, $method ) ) { | ||
| throw new PluginApiChangedException( | ||
| "{$class}::{$method}() does not exist even though the plugin was detected as active. Its API may have changed." | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether the product is a WooCommerce Subscriptions product. | ||
| * | ||
| * @param \WC_Product $product The product to check. | ||
| * @return bool | ||
| */ | ||
| private function is_customized_by_subscriptions( \WC_Product $product ): bool { | ||
| $this->assert_method_exists( \WC_Subscriptions_Product::class, 'is_subscription' ); | ||
|
|
||
| return \WC_Subscriptions_Product::is_subscription( $product ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether the product is a WooCommerce Gift Cards product. | ||
| * | ||
| * @param \WC_Product $product The product to check. | ||
| * @return bool | ||
| */ | ||
| private function is_customized_by_gift_cards( \WC_Product $product ): bool { | ||
| $this->assert_method_exists( \WC_GC_Gift_Card_Product::class, 'is_gift_card' ); | ||
|
|
||
| return \WC_GC_Gift_Card_Product::is_gift_card( $product ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether the product is a WooCommerce Product Bundles product. | ||
| * | ||
| * @param \WC_Product $product The product to check. | ||
| * @return bool | ||
| */ | ||
| private function is_customized_by_product_bundles( \WC_Product $product ): bool { | ||
| return $product->is_type( 'bundle' ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether the product has its own WooCommerce Product Add-Ons configured. | ||
| * | ||
| * Calls WC_Product_Addons_Helper::get_product_addons() with $inc_parent and | ||
| * $inc_global set to false, which returns only this product's own | ||
| * `_product_addons` meta instead of merging in global/category-level addon | ||
| * groups that are not specific to this product. | ||
| * | ||
| * @param \WC_Product $product The product to check. | ||
| * @return bool | ||
| */ | ||
| private function is_customized_by_product_addons( \WC_Product $product ): bool { | ||
| $this->assert_method_exists( \WC_Product_Addons_Helper::class, 'get_product_addons' ); | ||
|
|
||
| $addons = \WC_Product_Addons_Helper::get_product_addons( $product->get_id(), false, false, false ); | ||
|
|
||
| return array() !== $addons; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Rewrite as Reason: makes the decision a bit easier to understand
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
| } | ||
|
|
||
| /** | ||
| * Checks whether the product has its own WooCommerce Min/Max Quantities rules. | ||
| * | ||
| * Reads the product-level meta keys directly. The plugin itself has no | ||
| * product-only accessor for these two fields and reads the same meta keys | ||
| * directly internally; its only related method, get_group_of_quantity_for_product(), | ||
| * also merges in category-level term meta, which is not wanted here. | ||
| * | ||
| * Variations store these under differently named, `variation_`-prefixed | ||
| * meta keys instead of the plain ones used for simple/parent products. | ||
| * | ||
| * @param \WC_Product $product The product to check. | ||
| * @return bool | ||
| */ | ||
| private function is_customized_by_min_max_quantities( \WC_Product $product ): bool { | ||
| $meta_keys = $product instanceof \WC_Product_Variation | ||
| ? array( 'variation_minimum_allowed_quantity', 'variation_maximum_allowed_quantity', 'variation_group_of_quantity' ) | ||
| : array( 'minimum_allowed_quantity', 'maximum_allowed_quantity', 'group_of_quantity' ); | ||
|
|
||
| foreach ( $meta_keys as $meta_key ) { | ||
| if ( '' !== $product->get_meta( $meta_key, true ) ) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether the product is a WooCommerce Composite Products product. | ||
| * | ||
| * @param \WC_Product $product The product to check. | ||
| * @return bool | ||
| */ | ||
| private function is_customized_by_composite_products( \WC_Product $product ): bool { | ||
| return $product->is_type( 'composite' ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether the product has WooCommerce Per-Product Shipping enabled. | ||
| * | ||
| * Reads the `_per_product_shipping` meta directly. The plugin has no | ||
| * product-only accessor either: its own global function | ||
| * woocommerce_per_product_shipping_get_matching_rule() checks this same | ||
| * meta key internally, but requires a shipping package/destination | ||
| * context, so it cannot be used as a simple per-product boolean check. | ||
| * | ||
| * @param \WC_Product $product The product to check. | ||
| * @return bool | ||
| */ | ||
| private function is_customized_by_shipping_per_product( \WC_Product $product ): bool { | ||
| return 'yes' === $product->get_meta( '_per_product_shipping', true ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether the product has WooCommerce Deposits enabled. | ||
| * | ||
| * @param \WC_Product $product The product to check. | ||
| * @return bool | ||
| */ | ||
| private function is_customized_by_deposits( \WC_Product $product ): bool { | ||
| $this->assert_method_exists( \WC_Deposits_Product_Manager::class, 'deposits_enabled' ); | ||
|
|
||
| return \WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Remove the docblock
Reason: it's 100% noise, adding no value. PhpStorm expands the parent docblock automatically.
Of course, if the interface is removed (as I suggested), move the original docblock here 😉
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. The interface was removed, and I only left the file header here, removing the class docblock. It looks like in this project we often prefer file headers, so I followed the same pattern.