studiou-wc-mandatory-products.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <?php
  2. /**
  3. * Plugin Name: QDR - Studiou WC Mandatory Products
  4. * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-mandatory-products
  5. * Description: Extends WooCommerce to add mandatory products to cart based on product categories.
  6. * Version: 1.0.0
  7. * Requires at least: 6.7.2
  8. * Requires PHP: 8.2
  9. * Author: Dalibor Votruba
  10. * Author URI: https://www.quadarax.com
  11. * License: GPL v2 or later
  12. * License URI: https://www.gnu.org/licenses/gpl-2.0.html
  13. * Text Domain: studiou-wc-mandatory-products
  14. * Domain Path: /languages
  15. * WC requires at least: 3.0
  16. * WC tested up to: 8.0
  17. * Woo: 12345:a123b456c789d0
  18. */
  19. // Exit if accessed directly
  20. if (!defined('ABSPATH')) {
  21. exit;
  22. }
  23. // Check if WooCommerce is active
  24. if (!in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {
  25. return;
  26. }
  27. // Declare HPOS compatibility
  28. add_action('before_woocommerce_init', function() {
  29. if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) {
  30. \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
  31. 'custom_order_tables',
  32. __FILE__,
  33. true
  34. );
  35. }
  36. });
  37. if (!class_exists('StudioU_WC_Mandatory_Products')) {
  38. class StudioU_WC_Mandatory_Products {
  39. /**
  40. * Constructor
  41. */
  42. public function __construct() {
  43. // Define constants
  44. $this->define_constants();
  45. // Include required files
  46. $this->includes();
  47. // Initialize hooks
  48. $this->init_hooks();
  49. }
  50. /**
  51. * Define plugin constants
  52. */
  53. private function define_constants() {
  54. define('STUDIOU_WCMP_VERSION', '1.0.0');
  55. define('STUDIOU_WCMP_PLUGIN_DIR', plugin_dir_path(__FILE__));
  56. define('STUDIOU_WCMP_PLUGIN_URL', plugin_dir_url(__FILE__));
  57. }
  58. /**
  59. * Include required files
  60. */
  61. private function includes() {
  62. // Admin classes
  63. require_once STUDIOU_WCMP_PLUGIN_DIR . 'includes/class-studiou-wcmp-admin.php';
  64. // Frontend classes
  65. require_once STUDIOU_WCMP_PLUGIN_DIR . 'includes/class-studiou-wcmp-frontend.php';
  66. }
  67. /**
  68. * Initialize hooks
  69. */
  70. private function init_hooks() {
  71. // Activation hook
  72. register_activation_hook(__FILE__, array($this, 'activate'));
  73. // Admin hooks
  74. if (is_admin()) {
  75. add_action('init', array($this, 'init_admin'), 10);
  76. }
  77. // Frontend hooks
  78. add_action('init', array($this, 'init_frontend'), 10);
  79. // Add mandatory products to cart (with priority to ensure it runs after WooCommerce's own handlers)
  80. add_action('woocommerce_add_to_cart', array($this, 'add_mandatory_products_to_cart'), 20, 6);
  81. // Display mandatory products info on product page
  82. add_action('woocommerce_before_add_to_cart_button', array($this, 'display_mandatory_products_info'), 10);
  83. }
  84. /**
  85. * Plugin activation
  86. */
  87. public function activate() {
  88. // Nothing to do at activation for now
  89. }
  90. /**
  91. * Initialize admin
  92. */
  93. public function init_admin() {
  94. $admin = new StudioU_WCMP_Admin();
  95. $admin->init();
  96. }
  97. /**
  98. * Initialize frontend
  99. */
  100. public function init_frontend() {
  101. $frontend = new StudioU_WCMP_Frontend();
  102. $frontend->init();
  103. }
  104. /**
  105. * Get all mandatory products for a product
  106. *
  107. * @param int $product_id The product ID
  108. * @return array Array of mandatory products
  109. */
  110. public function get_mandatory_products($product_id) {
  111. $product_categories = get_the_terms($product_id, 'product_cat');
  112. if (!$product_categories || is_wp_error($product_categories)) {
  113. return array();
  114. }
  115. $mandatory_products = array();
  116. $processed_categories = array();
  117. // Get category lineage including parent categories
  118. foreach ($product_categories as $category) {
  119. $this->get_mandatory_products_from_category_lineage($category, $mandatory_products, $processed_categories);
  120. }
  121. return $mandatory_products;
  122. }
  123. /**
  124. * Recursive function to get mandatory products from a category and its parents
  125. *
  126. * @param object $category The category object
  127. * @param array &$mandatory_products Array to store mandatory products
  128. * @param array &$processed_categories Array to track processed categories to avoid duplicates
  129. */
  130. private function get_mandatory_products_from_category_lineage($category, &$mandatory_products, &$processed_categories) {
  131. // Skip if we've already processed this category
  132. if (in_array($category->term_id, $processed_categories)) {
  133. return;
  134. }
  135. // Mark as processed
  136. $processed_categories[] = $category->term_id;
  137. // Get mandatory products for this category
  138. $category_mandatory_products = get_term_meta($category->term_id, 'mandatory_products', true);
  139. if (!empty($category_mandatory_products) && is_array($category_mandatory_products)) {
  140. foreach ($category_mandatory_products as $mandatory_product) {
  141. $mandatory_products[] = $mandatory_product;
  142. }
  143. }
  144. // Process parent category if exists
  145. if ($category->parent > 0) {
  146. $parent_category = get_term($category->parent, 'product_cat');
  147. if ($parent_category && !is_wp_error($parent_category)) {
  148. $this->get_mandatory_products_from_category_lineage($parent_category, $mandatory_products, $processed_categories);
  149. }
  150. }
  151. }
  152. /**
  153. * Add mandatory products to cart
  154. *
  155. * @param string $cart_item_key Cart item key
  156. * @param int $product_id Product ID
  157. * @param int $quantity Quantity
  158. * @param int $variation_id Variation ID
  159. * @param array $variation Variation data
  160. * @param array $cart_item_data Cart item data
  161. */
  162. public function add_mandatory_products_to_cart($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
  163. // Don't execute for mandatory products themselves to avoid infinite loops
  164. if (isset($cart_item_data['mandatory_product'])) {
  165. return;
  166. }
  167. // Get mandatory products for this product
  168. $mandatory_products = $this->get_mandatory_products($product_id);
  169. if (empty($mandatory_products)) {
  170. return;
  171. }
  172. // Get cart contents
  173. $cart_contents = WC()->cart->get_cart();
  174. foreach ($mandatory_products as $mandatory_product) {
  175. $mandatory_product_id = absint($mandatory_product['product_id']);
  176. $mandatory_product_quantity = absint($mandatory_product['quantity']);
  177. $mandatory_product_type = sanitize_text_field($mandatory_product['type']);
  178. // Skip if product doesn't exist
  179. $product = wc_get_product($mandatory_product_id);
  180. if (!$product || !$product->is_purchasable() || !$product->is_in_stock()) {
  181. continue;
  182. }
  183. $add_to_cart = true;
  184. // Check if we need to add based on type
  185. if ($mandatory_product_type === 'OnePerCart') {
  186. // Check if product already exists in cart
  187. foreach ($cart_contents as $cart_item) {
  188. if ($cart_item['product_id'] == $mandatory_product_id) {
  189. $add_to_cart = false;
  190. break;
  191. }
  192. }
  193. } elseif ($mandatory_product_type === 'OnePerProduct') {
  194. // Check if this mandatory product has been added for this specific product already
  195. foreach ($cart_contents as $cart_item) {
  196. if (isset($cart_item['mandatory_for']) &&
  197. $cart_item['mandatory_for'] == $product_id &&
  198. $cart_item['product_id'] == $mandatory_product_id) {
  199. $add_to_cart = false;
  200. break;
  201. }
  202. }
  203. }
  204. // Add the mandatory product to cart if needed
  205. if ($add_to_cart) {
  206. // Add metadata to identify this as a mandatory product
  207. $extra_data = array(
  208. 'mandatory_product' => true,
  209. 'mandatory_for' => $product_id
  210. );
  211. // Add to cart
  212. WC()->cart->add_to_cart(
  213. $mandatory_product_id,
  214. $mandatory_product_quantity,
  215. 0, // No variation
  216. array(), // No variation data
  217. $extra_data
  218. );
  219. }
  220. }
  221. }
  222. /**
  223. * Display mandatory products info on product page
  224. */
  225. public function display_mandatory_products_info() {
  226. global $product;
  227. if (!$product) {
  228. return;
  229. }
  230. $mandatory_products = $this->get_mandatory_products($product->get_id());
  231. if (empty($mandatory_products)) {
  232. return;
  233. }
  234. echo '<div class="mandatory-products-info">';
  235. echo '<h4>' . __('This product includes the following mandatory items:', 'studiou-wc-mandatory-products') . '</h4>';
  236. echo '<ul>';
  237. foreach ($mandatory_products as $mandatory_product) {
  238. $product_id = absint($mandatory_product['product_id']);
  239. $quantity = absint($mandatory_product['quantity']);
  240. $type = sanitize_text_field($mandatory_product['type']);
  241. $mandatory_product_obj = wc_get_product($product_id);
  242. if ($mandatory_product_obj) {
  243. echo '<li>';
  244. echo sprintf(
  245. '<a href="%s">%s</a> x %d (%s)',
  246. get_permalink($product_id),
  247. $mandatory_product_obj->get_name(),
  248. $quantity,
  249. $type === 'OnePerCart' ? __('One per cart', 'studiou-wc-mandatory-products') : __('One per product', 'studiou-wc-mandatory-products')
  250. );
  251. echo '</li>';
  252. }
  253. }
  254. echo '</ul>';
  255. echo '</div>';
  256. }
  257. }
  258. // Initialize the plugin
  259. new StudioU_WC_Mandatory_Products();
  260. }