studiou-wc-mandatory-products.php 12 KB

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