studiou-wc-mandatory-products.php 11 KB

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