Skip to content

Instantly share code, notes, and snippets.

@melek
Last active August 26, 2022 00:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save melek/594d80882b4e0199a18a0c5df5baf3c7 to your computer and use it in GitHub Desktop.
Save melek/594d80882b4e0199a18a0c5df5baf3c7 to your computer and use it in GitHub Desktop.
An extension for WooCommerce Force Sells which lets a merchant make force sells optional on a product.

Optional Force Sells

This plugin adds an 'Optional Force Sells' checkbox in the 'Linked Products' tab of the Edit Product screen. When checked, the product's Single Product page will offer each force sell (normal and synced) as a checkbox instead of listing the products which will be added to the cart.

This plugin requires WooCommerce Force Sells to function.

Installation

There are two ways to install:

  • Download this gist as a ZIP file, rename it optional-force-sells.zip, and install it as a plugin in the WP Admin → Plugins area of your WordPress site.
  • Use Code Snippets to add the code as a snippet directly to your site. If you do this, remove the opening <?php line, since that is provided by Code Snippets.

How it works

Since there is no filter in Force Sells to select which product IDs are added, the plugin saves the list of selected force sells on the cart item, then checks each force sell's ID against the selections. If the force sell ID is not in the list when the product is added to the cart:

  • The id key for the force sell is set to null using the wc_force_sell_add_to_cart_product filter. This makes the add to cart attempt fail.
  • Filter wc_force_sell_disallow_no_stock to false, since otherwise Force Sells will not add the main item (or any items) to the cart if a force sell product wasn't added successfully.

Limitations:

  • This only affects the Single Product page. Adding to the cart elsewhere adds all the force sell items to the cart (the default Force Sells behavior).
  • There is no notice if the add-on can't be forced into the cart, and the initial list of force sells is not filtered by availability. Force sells which are selected but aren't available will silently fail to be added to the cart.
  • This plugin does not distinguish between synced and unsynced force sells or let you choose which force sells to make optional; it is all or nothing for each product.
  • To avoid name collision, this plugin uses the ofs_ function prefix. The likelihood of collision remains low - but it is higher than another method such as a singleton.
  • Some code is copied directly from Force Sells due to being inaccessible at runtime, so the plugin is fragile to implementation changes in Force Sells. That said, Force Sells has had stable data structures for quite some time that probably won't change anytime soon. The copied bits in the code aere noted for easy updating if needed.
<?php
/**
* Plugin Name: Optional Force Sells
* Plugin URI: https://gist.github.com/melek/594d80882b4e0199a18a0c5df5baf3c7
* Description: Extends WooCommerce Force Sells by allowing force sells on a product to be optional on the single product page.
* Version: 0.1.5
* Author: Lionel Di Giacomo
* Author URI: https://github.com/melek
* License: GPL 2.0+
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: woocommerce-optional-force-sells
*
* @package optional-force-sells
*/
// Only run if Force Sells is active.
if ( in_array( 'woocommerce-force-sells/woocommerce-force-sells.php', apply_filters( 'active_plugins', get_option( 'active_plugins' )))) {
add_action('wp_head', function () { echo "<style>.ofs-container { padding-bottom: 1.5rem; }</style>"; }); // A bit of custom CSS
add_action( 'woocommerce_before_add_to_cart_button', 'ofs_synced_product_add_ons', 9 ); // Front end: Show custom input field above Add to Cart
add_filter( 'woocommerce_add_cart_item_data', 'ofs_product_add_on_cart_item_data', 10, 2 ); // Save custom input field value into cart item data
add_filter( 'wc_force_sell_add_to_cart_product', 'ofs_filter_forced_sells', 10, 2); // Make sure the selected IDs are the only force-sells added to the cart
add_action( 'woocommerce_product_options_related', 'ofs_write_panel_tab', 11); // Render Optional Force Sells checkbox in Linked Products tab.
add_action( 'woocommerce_process_product_meta', 'ofs_process_extra_product_meta', 1, 2 ); // Save Optional Force Sell option in post meta when product is saved.
function ofs_synced_product_add_ons() {
if( !get_post_meta(get_the_ID(), "ofs_enabled", false) ) return;
// Remove the default single product display of force-sells
remove_action( 'woocommerce_after_add_to_cart_button', array(WC_Force_Sells::get_instance(), 'show_force_sell_products'));
echo '<div class="wc-force-sells ofs-container">' . esc_html__( 'Options', 'woocommerce-optional-force-sells' );
$fs_ids = ofs_get_force_sell_ids(get_the_ID(), array( 'normal', 'synced' ));
foreach($fs_ids as $fs_id) {
$fs_product = wc_get_product( $fs_id );
echo '<div class="ofs-single-product-option"><input type="checkbox" id="ofs_option_' . $fs_id . '" name="ofs_selected_force_sells[]" value="' . $fs_id . '">' .
'<label for="ofs_option_' . $fs_id . '">' . $fs_product->get_title() . ' - ' . $fs_product->get_price_html() . '</label></div>';
}
echo '<input type="hidden" name="ofs_selected_force_sells[]" value="">';
echo '</div>';
}
function ofs_product_add_on_cart_item_data( $cart_item, $product_id ){
if( isset($_POST['ofs_selected_force_sells']) ) {
$cart_item['ofs_selected_force_sells'] = $_POST['ofs_selected_force_sells'];
}
return $cart_item;
}
function ofs_filter_forced_sells($params, $cart_item) {
//wc_add_notice( '<pre>' . var_export($cart_item, true) . '</pre>', "notice"); // Uncomment to add a debug notice.
if( !isset( $_POST['ofs_selected_force_sells'] ) || !is_array($cart_item['ofs_selected_force_sells']) ) return $params;
// If optional force sells are present, allow main products to be added even if force sells aren't.
add_filter('wc_force_sell_disallow_no_stock', function($data) { return false; });
if( !in_array($params['id'], $cart_item['ofs_selected_force_sells']) ) $params['id'] = null;
return $params;
}
function ofs_write_panel_tab() {
?>
<p class="form-field">
<input id="ofs_enabled" type="checkbox" name="ofs_enabled"<?php if( get_post_meta( get_the_ID(), 'ofs_enabled', true) ) echo " checked"; ?>>
<label for="ofs_enabled"><?php esc_html_e( 'Optional Force Sells', 'woocommerce-optional-force-sells' ); ?></label>
<?php echo wc_help_tip( esc_html__( 'When checked, force sells on this product will appear on the product page with checkboxes. Only checked force sells will be added to the cart.', 'woocommerce-optional-force-sells' ) ); ?>
</p>
<?php
}
function ofs_process_extra_product_meta( $post_id, $post ) {
if ( isset( $_POST[ 'ofs_enabled' ] ) ) {
update_post_meta( $post_id, 'ofs_enabled', true);
} else {
delete_post_meta( $post_id, 'ofs_enabled');
}
}
// Some Force Sells functionality is private, so this duplicates the 'get_force_sell_ids' function with the $synced_types array built-in.
function ofs_get_force_sell_ids( $product_id, $types ) {
// Array & function are private, so copied from woocommerce-force-sells.php:38
// Force Sells version 1.1.31
$synced_types = array(
'normal' => array(
'field_name' => 'force_sell_ids',
'meta_name' => '_force_sell_ids',
),
'synced' => array(
'field_name' => 'force_sell_synced_ids',
'meta_name' => '_force_sell_synced_ids',
),
);
// Function is private, so copied from woocommerce-force-sells.php:457
// Force Sells version 1.1.31
if ( ! is_array( $types ) || empty( $types ) ) {
return array();
}
$ids = array();
foreach ( $types as $type ) {
$new_ids = array();
if ( isset( $synced_types[ $type ] ) ) {
$new_ids = get_post_meta( $product_id, $synced_types[ $type ]['meta_name'], true );
if ( is_array( $new_ids ) && ! empty( $new_ids ) ) {
$ids = array_merge( $ids, $new_ids );
}
}
}
return $ids;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment