class-db-manager.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. <?php
  2. /**
  3. * Database Manager Class - Handles all database operations (HPOS Compatible)
  4. *
  5. * @package StudiouWCCustomReports
  6. */
  7. if (!defined('ABSPATH')) {
  8. exit;
  9. }
  10. class StudiouWC_DB_Manager {
  11. public function get_product_categories_summary($date_from, $date_to, $statuses, $per_page, $paged, $orderby, $order) {
  12. global $wpdb;
  13. if (empty($statuses)) {
  14. return array();
  15. }
  16. $settings_manager = new StudiouWC_Settings_Manager();
  17. $settings = $settings_manager->get_settings();
  18. $default_property = $settings['default_property'] ?? '';
  19. $statuses_placeholder = implode(',', array_fill(0, count($statuses), '%s'));
  20. $offset = ($paged - 1) * $per_page;
  21. // Check if HPOS is enabled
  22. if ($this->is_hpos_enabled()) {
  23. $sql = $this->get_hpos_categories_query($statuses_placeholder);
  24. } else {
  25. $sql = $this->get_legacy_categories_query($statuses_placeholder);
  26. }
  27. // Add ordering
  28. $sql .= $this->get_order_clause($orderby, $order);
  29. $sql .= " LIMIT %d OFFSET %d";
  30. $query_params = array_merge(
  31. array($date_from, $date_to),
  32. $statuses,
  33. array($per_page, $offset)
  34. );
  35. $results = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
  36. // Get property counts for each category if property is set
  37. foreach ($results as &$result) {
  38. $result['property_counts'] = array();
  39. if (!empty($default_property)) {
  40. $result['property_counts'] = $this->get_property_counts_for_category(
  41. $result['category'],
  42. $date_from,
  43. $date_to,
  44. $statuses,
  45. $default_property
  46. );
  47. }
  48. }
  49. return $results;
  50. }
  51. public function get_property_counts_for_category($category, $date_from, $date_to, $statuses, $property) {
  52. global $wpdb;
  53. if (empty($statuses)) {
  54. return array();
  55. }
  56. $statuses_placeholder = implode(',', array_fill(0, count($statuses), '%s'));
  57. $taxonomy = 'pa_' . $property; // Add the pa_ prefix for WooCommerce attributes
  58. if ($this->is_hpos_enabled()) {
  59. $sql = $this->get_hpos_property_counts_query($statuses_placeholder);
  60. } else {
  61. $sql = $this->get_legacy_property_counts_query($statuses_placeholder);
  62. }
  63. $query_params = array_merge(
  64. array($date_from, $date_to),
  65. $statuses,
  66. array($category, $taxonomy)
  67. );
  68. $results = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
  69. $property_counts = array();
  70. foreach ($results as $result) {
  71. if (!empty($result['property_value'])) {
  72. $property_counts[$result['property_value']] = $result['count'];
  73. }
  74. }
  75. return $property_counts;
  76. }
  77. public function get_total_items($date_from, $date_to, $statuses) {
  78. global $wpdb;
  79. if (empty($statuses)) {
  80. return 0;
  81. }
  82. $statuses_placeholder = implode(',', array_fill(0, count($statuses), '%s'));
  83. if ($this->is_hpos_enabled()) {
  84. $sql = $this->get_hpos_total_items_query($statuses_placeholder);
  85. } else {
  86. $sql = $this->get_legacy_total_items_query($statuses_placeholder);
  87. }
  88. $query_params = array_merge(
  89. array($date_from, $date_to),
  90. $statuses
  91. );
  92. return $wpdb->get_var($wpdb->prepare($sql, $query_params));
  93. }
  94. /**
  95. * Get product attributes (not meta keys) for dropdown
  96. */
  97. public function get_product_properties() {
  98. global $wpdb;
  99. // Get WooCommerce product attributes
  100. $attributes = $wpdb->get_results("
  101. SELECT attribute_name, attribute_label
  102. FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
  103. ORDER BY attribute_label
  104. ", ARRAY_A);
  105. $properties = array();
  106. foreach ($attributes as $attribute) {
  107. $properties[$attribute['attribute_name']] = $attribute['attribute_label'];
  108. }
  109. return $properties;
  110. }
  111. /**
  112. * Get property values for a specific attribute
  113. */
  114. public function get_property_values($property) {
  115. global $wpdb;
  116. if (empty($property)) {
  117. return array();
  118. }
  119. // Build taxonomy name for WooCommerce attributes
  120. $taxonomy = 'pa_' . $property;
  121. // Get terms for this attribute taxonomy
  122. $sql = "
  123. SELECT t.name, t.slug
  124. FROM {$wpdb->terms} t
  125. INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
  126. WHERE tt.taxonomy = %s
  127. ORDER BY t.name
  128. ";
  129. $results = $wpdb->get_results($wpdb->prepare($sql, $taxonomy), ARRAY_A);
  130. $values = array();
  131. foreach ($results as $result) {
  132. $values[$result['slug']] = $result['name'];
  133. }
  134. return $values;
  135. }
  136. /**
  137. * Get attribute name from attribute slug
  138. */
  139. public function get_attribute_name($attribute_slug) {
  140. global $wpdb;
  141. $result = $wpdb->get_var($wpdb->prepare("
  142. SELECT attribute_label
  143. FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
  144. WHERE attribute_name = %s
  145. ", $attribute_slug));
  146. return $result ? $result : $attribute_slug;
  147. }
  148. /**
  149. * Check if HPOS (High-Performance Order Storage) is enabled
  150. */
  151. private function is_hpos_enabled() {
  152. return class_exists('Automattic\WooCommerce\Utilities\OrderUtil') &&
  153. \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
  154. }
  155. /**
  156. * Get HPOS compatible query for categories summary
  157. */
  158. private function get_hpos_categories_query($statuses_placeholder) {
  159. global $wpdb;
  160. return "
  161. SELECT
  162. t.name as category,
  163. COUNT(DISTINCT o.id) as orders_count,
  164. SUM(oi.product_net_revenue) as total_price
  165. FROM {$wpdb->prefix}wc_orders o
  166. INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON o.id = oi.order_id
  167. INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
  168. INNER JOIN {$wpdb->term_relationships} tr ON (
  169. CASE
  170. WHEN oi.variation_id > 0 THEN
  171. (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
  172. ELSE oi.product_id
  173. END
  174. ) = tr.object_id
  175. INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
  176. INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
  177. WHERE o.date_created_gmt >= %s
  178. AND o.date_created_gmt <= %s
  179. AND o.status IN ($statuses_placeholder)
  180. AND tt.taxonomy = 'product_cat'
  181. GROUP BY t.term_id, t.name
  182. ";
  183. }
  184. /**
  185. * Get legacy query for categories summary (pre-HPOS)
  186. */
  187. private function get_legacy_categories_query($statuses_placeholder) {
  188. global $wpdb;
  189. return "
  190. SELECT
  191. t.name as category,
  192. COUNT(DISTINCT p_order.ID) as orders_count,
  193. SUM(oi.product_net_revenue) as total_price
  194. FROM {$wpdb->posts} p_order
  195. INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON p_order.ID = oi.order_id
  196. INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
  197. INNER JOIN {$wpdb->term_relationships} tr ON (
  198. CASE
  199. WHEN oi.variation_id > 0 THEN
  200. (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
  201. ELSE oi.product_id
  202. END
  203. ) = tr.object_id
  204. INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
  205. INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
  206. WHERE p_order.post_date >= %s
  207. AND p_order.post_date <= %s
  208. AND p_order.post_status IN ($statuses_placeholder)
  209. AND p_order.post_type = 'shop_order'
  210. AND tt.taxonomy = 'product_cat'
  211. GROUP BY t.term_id, t.name
  212. ";
  213. }
  214. /**
  215. * Get HPOS compatible query for property counts
  216. */
  217. private function get_hpos_property_counts_query($statuses_placeholder) {
  218. global $wpdb;
  219. return "
  220. SELECT
  221. t_attr.slug as property_value,
  222. COUNT(DISTINCT oi.order_item_id) as count
  223. FROM {$wpdb->prefix}wc_orders o
  224. INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON o.id = oi.order_id
  225. INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
  226. INNER JOIN {$wpdb->term_relationships} tr_cat ON (
  227. CASE
  228. WHEN oi.variation_id > 0 THEN
  229. (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
  230. ELSE oi.product_id
  231. END
  232. ) = tr_cat.object_id
  233. INNER JOIN {$wpdb->term_taxonomy} tt_cat ON tr_cat.term_taxonomy_id = tt_cat.term_taxonomy_id
  234. INNER JOIN {$wpdb->terms} t_cat ON tt_cat.term_id = t_cat.term_id
  235. INNER JOIN {$wpdb->term_relationships} tr_attr ON (
  236. CASE
  237. WHEN oi.variation_id > 0 THEN oi.variation_id
  238. ELSE oi.product_id
  239. END
  240. ) = tr_attr.object_id
  241. INNER JOIN {$wpdb->term_taxonomy} tt_attr ON tr_attr.term_taxonomy_id = tt_attr.term_taxonomy_id
  242. INNER JOIN {$wpdb->terms} t_attr ON tt_attr.term_id = t_attr.term_id
  243. WHERE o.date_created_gmt >= %s
  244. AND o.date_created_gmt <= %s
  245. AND o.status IN ($statuses_placeholder)
  246. AND tt_cat.taxonomy = 'product_cat'
  247. AND t_cat.name = %s
  248. AND tt_attr.taxonomy = %s
  249. GROUP BY t_attr.term_id, t_attr.slug
  250. ";
  251. }
  252. /**
  253. * Get legacy query for property counts (pre-HPOS)
  254. */
  255. private function get_legacy_property_counts_query($statuses_placeholder) {
  256. global $wpdb;
  257. return "
  258. SELECT
  259. t_attr.slug as property_value,
  260. COUNT(DISTINCT oi.order_item_id) as count
  261. FROM {$wpdb->posts} p_order
  262. INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON p_order.ID = oi.order_id
  263. INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
  264. INNER JOIN {$wpdb->term_relationships} tr_cat ON (
  265. CASE
  266. WHEN oi.variation_id > 0 THEN
  267. (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
  268. ELSE oi.product_id
  269. END
  270. ) = tr_cat.object_id
  271. INNER JOIN {$wpdb->term_taxonomy} tt_cat ON tr_cat.term_taxonomy_id = tt_cat.term_taxonomy_id
  272. INNER JOIN {$wpdb->terms} t_cat ON tt_cat.term_id = t_cat.term_id
  273. INNER JOIN {$wpdb->term_relationships} tr_attr ON (
  274. CASE
  275. WHEN oi.variation_id > 0 THEN oi.variation_id
  276. ELSE oi.product_id
  277. END
  278. ) = tr_attr.object_id
  279. INNER JOIN {$wpdb->term_taxonomy} tt_attr ON tr_attr.term_taxonomy_id = tt_attr.term_taxonomy_id
  280. INNER JOIN {$wpdb->terms} t_attr ON tt_attr.term_id = t_attr.term_id
  281. WHERE p_order.post_date >= %s
  282. AND p_order.post_date <= %s
  283. AND p_order.post_status IN ($statuses_placeholder)
  284. AND p_order.post_type = 'shop_order'
  285. AND tt_cat.taxonomy = 'product_cat'
  286. AND t_cat.name = %s
  287. AND tt_attr.taxonomy = %s
  288. GROUP BY t_attr.term_id, t_attr.slug
  289. ";
  290. }
  291. /**
  292. * Get HPOS compatible query for total items
  293. */
  294. private function get_hpos_total_items_query($statuses_placeholder) {
  295. global $wpdb;
  296. return "
  297. SELECT COUNT(DISTINCT t.term_id)
  298. FROM {$wpdb->prefix}wc_orders o
  299. INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON o.id = oi.order_id
  300. INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
  301. INNER JOIN {$wpdb->term_relationships} tr ON (
  302. CASE
  303. WHEN oi.variation_id > 0 THEN
  304. (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
  305. ELSE oi.product_id
  306. END
  307. ) = tr.object_id
  308. INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
  309. INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
  310. WHERE o.date_created_gmt >= %s
  311. AND o.date_created_gmt <= %s
  312. AND o.status IN ($statuses_placeholder)
  313. AND tt.taxonomy = 'product_cat'
  314. ";
  315. }
  316. /**
  317. * Get legacy query for total items (pre-HPOS)
  318. */
  319. private function get_legacy_total_items_query($statuses_placeholder) {
  320. global $wpdb;
  321. return "
  322. SELECT COUNT(DISTINCT t.term_id)
  323. FROM {$wpdb->posts} p_order
  324. INNER JOIN {$wpdb->prefix}wc_order_product_lookup oi ON p_order.ID = oi.order_id
  325. INNER JOIN {$wpdb->posts} p ON oi.product_id = p.ID OR oi.variation_id = p.ID
  326. INNER JOIN {$wpdb->term_relationships} tr ON (
  327. CASE
  328. WHEN oi.variation_id > 0 THEN
  329. (SELECT post_parent FROM {$wpdb->posts} WHERE ID = oi.variation_id)
  330. ELSE oi.product_id
  331. END
  332. ) = tr.object_id
  333. INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
  334. INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
  335. WHERE p_order.post_date >= %s
  336. AND p_order.post_date <= %s
  337. AND p_order.post_status IN ($statuses_placeholder)
  338. AND p_order.post_type = 'shop_order'
  339. AND tt.taxonomy = 'product_cat'
  340. ";
  341. }
  342. private function get_order_clause($orderby, $order) {
  343. $order = ($order === 'DESC') ? 'DESC' : 'ASC';
  344. switch ($orderby) {
  345. case 'orders_count':
  346. return " ORDER BY orders_count $order";
  347. case 'total_price':
  348. return " ORDER BY total_price $order";
  349. case 'category':
  350. default:
  351. return " ORDER BY t.name $order";
  352. }
  353. }
  354. }