class-wc-product-data-store-cpt.php 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762
  1. <?php
  2. /**
  3. * WC_Product_Data_Store_CPT class file.
  4. *
  5. * @package WooCommerce/Classes
  6. */
  7. if ( ! defined( 'ABSPATH' ) ) {
  8. exit;
  9. }
  10. /**
  11. * WC Product Data Store: Stored in CPT.
  12. *
  13. * @version 3.0.0
  14. */
  15. class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Product_Data_Store_Interface {
  16. /**
  17. * Data stored in meta keys, but not considered "meta".
  18. *
  19. * @since 3.0.0
  20. * @var array
  21. */
  22. protected $internal_meta_keys = array(
  23. '_visibility',
  24. '_sku',
  25. '_price',
  26. '_regular_price',
  27. '_sale_price',
  28. '_sale_price_dates_from',
  29. '_sale_price_dates_to',
  30. 'total_sales',
  31. '_tax_status',
  32. '_tax_class',
  33. '_manage_stock',
  34. '_stock',
  35. '_stock_status',
  36. '_backorders',
  37. '_sold_individually',
  38. '_weight',
  39. '_length',
  40. '_width',
  41. '_height',
  42. '_upsell_ids',
  43. '_crosssell_ids',
  44. '_purchase_note',
  45. '_default_attributes',
  46. '_product_attributes',
  47. '_virtual',
  48. '_downloadable',
  49. '_download_limit',
  50. '_download_expiry',
  51. '_featured',
  52. '_downloadable_files',
  53. '_wc_rating_count',
  54. '_wc_average_rating',
  55. '_wc_review_count',
  56. '_variation_description',
  57. '_thumbnail_id',
  58. '_file_paths',
  59. '_product_image_gallery',
  60. '_product_version',
  61. '_wp_old_slug',
  62. '_edit_last',
  63. '_edit_lock',
  64. );
  65. /**
  66. * If we have already saved our extra data, don't do automatic / default handling.
  67. *
  68. * @var bool
  69. */
  70. protected $extra_data_saved = false;
  71. /**
  72. * Stores updated props.
  73. *
  74. * @var array
  75. */
  76. protected $updated_props = array();
  77. /*
  78. |--------------------------------------------------------------------------
  79. | CRUD Methods
  80. |--------------------------------------------------------------------------
  81. */
  82. /**
  83. * Method to create a new product in the database.
  84. *
  85. * @param WC_Product $product Product object.
  86. */
  87. public function create( &$product ) {
  88. if ( ! $product->get_date_created( 'edit' ) ) {
  89. $product->set_date_created( current_time( 'timestamp', true ) );
  90. }
  91. $id = wp_insert_post(
  92. apply_filters(
  93. 'woocommerce_new_product_data', array(
  94. 'post_type' => 'product',
  95. 'post_status' => $product->get_status() ? $product->get_status() : 'publish',
  96. 'post_author' => get_current_user_id(),
  97. 'post_title' => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ),
  98. 'post_content' => $product->get_description(),
  99. 'post_excerpt' => $product->get_short_description(),
  100. 'post_parent' => $product->get_parent_id(),
  101. 'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed',
  102. 'ping_status' => 'closed',
  103. 'menu_order' => $product->get_menu_order(),
  104. 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
  105. 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
  106. 'post_name' => $product->get_slug( 'edit' ),
  107. )
  108. ), true
  109. );
  110. if ( $id && ! is_wp_error( $id ) ) {
  111. $product->set_id( $id );
  112. $this->update_post_meta( $product, true );
  113. $this->update_terms( $product, true );
  114. $this->update_visibility( $product, true );
  115. $this->update_attributes( $product, true );
  116. $this->update_version_and_type( $product );
  117. $this->handle_updated_props( $product );
  118. $product->save_meta_data();
  119. $product->apply_changes();
  120. $this->clear_caches( $product );
  121. do_action( 'woocommerce_new_product', $id );
  122. }
  123. }
  124. /**
  125. * Method to read a product from the database.
  126. *
  127. * @param WC_Product $product Product object.
  128. * @throws Exception If invalid product.
  129. */
  130. public function read( &$product ) {
  131. $product->set_defaults();
  132. $post_object = get_post( $product->get_id() );
  133. if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) {
  134. throw new Exception( __( 'Invalid product.', 'woocommerce' ) );
  135. }
  136. $product->set_props(
  137. array(
  138. 'name' => $post_object->post_title,
  139. 'slug' => $post_object->post_name,
  140. 'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
  141. 'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
  142. 'status' => $post_object->post_status,
  143. 'description' => $post_object->post_content,
  144. 'short_description' => $post_object->post_excerpt,
  145. 'parent_id' => $post_object->post_parent,
  146. 'menu_order' => $post_object->menu_order,
  147. 'reviews_allowed' => 'open' === $post_object->comment_status,
  148. )
  149. );
  150. $this->read_attributes( $product );
  151. $this->read_downloads( $product );
  152. $this->read_visibility( $product );
  153. $this->read_product_data( $product );
  154. $this->read_extra_data( $product );
  155. $product->set_object_read( true );
  156. }
  157. /**
  158. * Method to update a product in the database.
  159. *
  160. * @param WC_Product $product Product object.
  161. */
  162. public function update( &$product ) {
  163. $product->save_meta_data();
  164. $changes = $product->get_changes();
  165. // Only update the post when the post data changes.
  166. if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) {
  167. $post_data = array(
  168. 'post_content' => $product->get_description( 'edit' ),
  169. 'post_excerpt' => $product->get_short_description( 'edit' ),
  170. 'post_title' => $product->get_name( 'edit' ),
  171. 'post_parent' => $product->get_parent_id( 'edit' ),
  172. 'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed',
  173. 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
  174. 'menu_order' => $product->get_menu_order( 'edit' ),
  175. 'post_name' => $product->get_slug( 'edit' ),
  176. 'post_type' => 'product',
  177. );
  178. if ( $product->get_date_created( 'edit' ) ) {
  179. $post_data['post_date'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() );
  180. $post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() );
  181. }
  182. if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) {
  183. $post_data['post_modified'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() );
  184. $post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() );
  185. } else {
  186. $post_data['post_modified'] = current_time( 'mysql' );
  187. $post_data['post_modified_gmt'] = current_time( 'mysql', 1 );
  188. }
  189. /**
  190. * When updating this object, to prevent infinite loops, use $wpdb
  191. * to update data, since wp_update_post spawns more calls to the
  192. * save_post action.
  193. *
  194. * This ensures hooks are fired by either WP itself (admin screen save),
  195. * or an update purely from CRUD.
  196. */
  197. if ( doing_action( 'save_post' ) ) {
  198. $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
  199. clean_post_cache( $product->get_id() );
  200. } else {
  201. wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
  202. }
  203. $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
  204. } else { // Only update post modified time to record this save event.
  205. $GLOBALS['wpdb']->update(
  206. $GLOBALS['wpdb']->posts,
  207. array(
  208. 'post_modified' => current_time( 'mysql' ),
  209. 'post_modified_gmt' => current_time( 'mysql', 1 ),
  210. ),
  211. array(
  212. 'ID' => $product->get_id(),
  213. )
  214. );
  215. clean_post_cache( $product->get_id() );
  216. }
  217. $this->update_post_meta( $product );
  218. $this->update_terms( $product );
  219. $this->update_visibility( $product );
  220. $this->update_attributes( $product );
  221. $this->update_version_and_type( $product );
  222. $this->handle_updated_props( $product );
  223. $product->apply_changes();
  224. $this->clear_caches( $product );
  225. do_action( 'woocommerce_update_product', $product->get_id() );
  226. }
  227. /**
  228. * Method to delete a product from the database.
  229. *
  230. * @param WC_Product $product Product object.
  231. * @param array $args Array of args to pass to the delete method.
  232. */
  233. public function delete( &$product, $args = array() ) {
  234. $id = $product->get_id();
  235. $post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product';
  236. $args = wp_parse_args(
  237. $args, array(
  238. 'force_delete' => false,
  239. )
  240. );
  241. if ( ! $id ) {
  242. return;
  243. }
  244. if ( $args['force_delete'] ) {
  245. do_action( 'woocommerce_before_delete_' . $post_type, $id );
  246. wp_delete_post( $id );
  247. $product->set_id( 0 );
  248. do_action( 'woocommerce_delete_' . $post_type, $id );
  249. } else {
  250. wp_trash_post( $id );
  251. $product->set_status( 'trash' );
  252. do_action( 'woocommerce_trash_' . $post_type, $id );
  253. }
  254. }
  255. /*
  256. |--------------------------------------------------------------------------
  257. | Additional Methods
  258. |--------------------------------------------------------------------------
  259. */
  260. /**
  261. * Read product data. Can be overridden by child classes to load other props.
  262. *
  263. * @param WC_Product $product Product object.
  264. * @since 3.0.0
  265. */
  266. protected function read_product_data( &$product ) {
  267. $id = $product->get_id();
  268. $review_count = get_post_meta( $id, '_wc_review_count', true );
  269. $rating_counts = get_post_meta( $id, '_wc_rating_count', true );
  270. $average_rating = get_post_meta( $id, '_wc_average_rating', true );
  271. if ( '' === $review_count ) {
  272. WC_Comments::get_review_count_for_product( $product );
  273. } else {
  274. $product->set_review_count( $review_count );
  275. }
  276. if ( '' === $rating_counts ) {
  277. WC_Comments::get_rating_counts_for_product( $product );
  278. } else {
  279. $product->set_rating_counts( $rating_counts );
  280. }
  281. if ( '' === $average_rating ) {
  282. WC_Comments::get_average_rating_for_product( $product );
  283. } else {
  284. $product->set_average_rating( $average_rating );
  285. }
  286. $product->set_props(
  287. array(
  288. 'sku' => get_post_meta( $id, '_sku', true ),
  289. 'regular_price' => get_post_meta( $id, '_regular_price', true ),
  290. 'sale_price' => get_post_meta( $id, '_sale_price', true ),
  291. 'price' => get_post_meta( $id, '_price', true ),
  292. 'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ),
  293. 'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ),
  294. 'total_sales' => get_post_meta( $id, 'total_sales', true ),
  295. 'tax_status' => get_post_meta( $id, '_tax_status', true ),
  296. 'tax_class' => get_post_meta( $id, '_tax_class', true ),
  297. 'manage_stock' => get_post_meta( $id, '_manage_stock', true ),
  298. 'stock_quantity' => get_post_meta( $id, '_stock', true ),
  299. 'stock_status' => get_post_meta( $id, '_stock_status', true ),
  300. 'backorders' => get_post_meta( $id, '_backorders', true ),
  301. 'sold_individually' => get_post_meta( $id, '_sold_individually', true ),
  302. 'weight' => get_post_meta( $id, '_weight', true ),
  303. 'length' => get_post_meta( $id, '_length', true ),
  304. 'width' => get_post_meta( $id, '_width', true ),
  305. 'height' => get_post_meta( $id, '_height', true ),
  306. 'upsell_ids' => get_post_meta( $id, '_upsell_ids', true ),
  307. 'cross_sell_ids' => get_post_meta( $id, '_crosssell_ids', true ),
  308. 'purchase_note' => get_post_meta( $id, '_purchase_note', true ),
  309. 'default_attributes' => get_post_meta( $id, '_default_attributes', true ),
  310. 'category_ids' => $this->get_term_ids( $product, 'product_cat' ),
  311. 'tag_ids' => $this->get_term_ids( $product, 'product_tag' ),
  312. 'shipping_class_id' => current( $this->get_term_ids( $product, 'product_shipping_class' ) ),
  313. 'virtual' => get_post_meta( $id, '_virtual', true ),
  314. 'downloadable' => get_post_meta( $id, '_downloadable', true ),
  315. 'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ),
  316. 'download_limit' => get_post_meta( $id, '_download_limit', true ),
  317. 'download_expiry' => get_post_meta( $id, '_download_expiry', true ),
  318. 'image_id' => get_post_thumbnail_id( $id ),
  319. )
  320. );
  321. // Handle sale dates on the fly in case of missed cron schedule.
  322. if ( $product->is_type( 'simple' ) && $product->is_on_sale( 'edit' ) && $product->get_sale_price( 'edit' ) !== $product->get_price( 'edit' ) ) {
  323. update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) );
  324. $product->set_price( $product->get_sale_price( 'edit' ) );
  325. }
  326. }
  327. /**
  328. * Read extra data associated with the product, like button text or product URL for external products.
  329. *
  330. * @param WC_Product $product Product object.
  331. * @since 3.0.0
  332. */
  333. protected function read_extra_data( &$product ) {
  334. foreach ( $product->get_extra_data_keys() as $key ) {
  335. $function = 'set_' . $key;
  336. if ( is_callable( array( $product, $function ) ) ) {
  337. $product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) );
  338. }
  339. }
  340. }
  341. /**
  342. * Convert visibility terms to props.
  343. * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'.
  344. *
  345. * @param WC_Product $product Product object.
  346. * @since 3.0.0
  347. */
  348. protected function read_visibility( &$product ) {
  349. $terms = get_the_terms( $product->get_id(), 'product_visibility' );
  350. $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
  351. $featured = in_array( 'featured', $term_names, true );
  352. $exclude_search = in_array( 'exclude-from-search', $term_names, true );
  353. $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );
  354. if ( $exclude_search && $exclude_catalog ) {
  355. $catalog_visibility = 'hidden';
  356. } elseif ( $exclude_search ) {
  357. $catalog_visibility = 'catalog';
  358. } elseif ( $exclude_catalog ) {
  359. $catalog_visibility = 'search';
  360. } else {
  361. $catalog_visibility = 'visible';
  362. }
  363. $product->set_props(
  364. array(
  365. 'featured' => $featured,
  366. 'catalog_visibility' => $catalog_visibility,
  367. )
  368. );
  369. }
  370. /**
  371. * Read attributes from post meta.
  372. *
  373. * @param WC_Product $product Product object.
  374. */
  375. protected function read_attributes( &$product ) {
  376. $meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );
  377. if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
  378. $attributes = array();
  379. foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
  380. $meta_value = array_merge(
  381. array(
  382. 'name' => '',
  383. 'value' => '',
  384. 'position' => 0,
  385. 'is_visible' => 0,
  386. 'is_variation' => 0,
  387. 'is_taxonomy' => 0,
  388. ), (array) $meta_attribute_value
  389. );
  390. // Check if is a taxonomy attribute.
  391. if ( ! empty( $meta_value['is_taxonomy'] ) ) {
  392. if ( ! taxonomy_exists( $meta_value['name'] ) ) {
  393. continue;
  394. }
  395. $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
  396. $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
  397. } else {
  398. $id = 0;
  399. $options = wc_get_text_attributes( $meta_value['value'] );
  400. }
  401. $attribute = new WC_Product_Attribute();
  402. $attribute->set_id( $id );
  403. $attribute->set_name( $meta_value['name'] );
  404. $attribute->set_options( $options );
  405. $attribute->set_position( $meta_value['position'] );
  406. $attribute->set_visible( $meta_value['is_visible'] );
  407. $attribute->set_variation( $meta_value['is_variation'] );
  408. $attributes[] = $attribute;
  409. }
  410. $product->set_attributes( $attributes );
  411. }
  412. }
  413. /**
  414. * Read downloads from post meta.
  415. *
  416. * @param WC_Product $product Product object.
  417. * @since 3.0.0
  418. */
  419. protected function read_downloads( &$product ) {
  420. $meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) );
  421. if ( $meta_values ) {
  422. $downloads = array();
  423. foreach ( $meta_values as $key => $value ) {
  424. if ( ! isset( $value['name'], $value['file'] ) ) {
  425. continue;
  426. }
  427. $download = new WC_Product_Download();
  428. $download->set_id( $key );
  429. $download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) );
  430. $download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) );
  431. $downloads[] = $download;
  432. }
  433. $product->set_downloads( $downloads );
  434. }
  435. }
  436. /**
  437. * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
  438. *
  439. * @param WC_Product $product Product object.
  440. * @param bool $force Force update. Used during create.
  441. * @since 3.0.0
  442. */
  443. protected function update_post_meta( &$product, $force = false ) {
  444. $meta_key_to_props = array(
  445. '_sku' => 'sku',
  446. '_regular_price' => 'regular_price',
  447. '_sale_price' => 'sale_price',
  448. '_sale_price_dates_from' => 'date_on_sale_from',
  449. '_sale_price_dates_to' => 'date_on_sale_to',
  450. 'total_sales' => 'total_sales',
  451. '_tax_status' => 'tax_status',
  452. '_tax_class' => 'tax_class',
  453. '_manage_stock' => 'manage_stock',
  454. '_backorders' => 'backorders',
  455. '_sold_individually' => 'sold_individually',
  456. '_weight' => 'weight',
  457. '_length' => 'length',
  458. '_width' => 'width',
  459. '_height' => 'height',
  460. '_upsell_ids' => 'upsell_ids',
  461. '_crosssell_ids' => 'cross_sell_ids',
  462. '_purchase_note' => 'purchase_note',
  463. '_default_attributes' => 'default_attributes',
  464. '_virtual' => 'virtual',
  465. '_downloadable' => 'downloadable',
  466. '_product_image_gallery' => 'gallery_image_ids',
  467. '_download_limit' => 'download_limit',
  468. '_download_expiry' => 'download_expiry',
  469. '_thumbnail_id' => 'image_id',
  470. '_stock' => 'stock_quantity',
  471. '_stock_status' => 'stock_status',
  472. '_wc_average_rating' => 'average_rating',
  473. '_wc_rating_count' => 'rating_counts',
  474. '_wc_review_count' => 'review_count',
  475. );
  476. // Make sure to take extra data (like product url or text for external products) into account.
  477. $extra_data_keys = $product->get_extra_data_keys();
  478. foreach ( $extra_data_keys as $key ) {
  479. $meta_key_to_props[ '_' . $key ] = $key;
  480. }
  481. $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );
  482. foreach ( $props_to_update as $meta_key => $prop ) {
  483. $value = $product->{"get_$prop"}( 'edit' );
  484. $value = is_string( $value ) ? wp_slash( $value ) : $value;
  485. switch ( $prop ) {
  486. case 'virtual':
  487. case 'downloadable':
  488. case 'manage_stock':
  489. case 'sold_individually':
  490. $updated = update_post_meta( $product->get_id(), $meta_key, wc_bool_to_string( $value ) );
  491. break;
  492. case 'gallery_image_ids':
  493. $updated = update_post_meta( $product->get_id(), $meta_key, implode( ',', $value ) );
  494. break;
  495. case 'image_id':
  496. if ( ! empty( $value ) ) {
  497. set_post_thumbnail( $product->get_id(), $value );
  498. } else {
  499. delete_post_meta( $product->get_id(), '_thumbnail_id' );
  500. }
  501. $updated = true;
  502. break;
  503. case 'date_on_sale_from':
  504. case 'date_on_sale_to':
  505. $updated = update_post_meta( $product->get_id(), $meta_key, $value ? $value->getTimestamp() : '' );
  506. break;
  507. default:
  508. $updated = update_post_meta( $product->get_id(), $meta_key, $value );
  509. break;
  510. }
  511. if ( $updated ) {
  512. $this->updated_props[] = $prop;
  513. }
  514. }
  515. // Update extra data associated with the product like button text or product URL for external products.
  516. if ( ! $this->extra_data_saved ) {
  517. foreach ( $extra_data_keys as $key ) {
  518. if ( ! array_key_exists( '_' . $key, $props_to_update ) ) {
  519. continue;
  520. }
  521. $function = 'get_' . $key;
  522. if ( is_callable( array( $product, $function ) ) ) {
  523. $value = $product->{$function}( 'edit' );
  524. $value = is_string( $value ) ? wp_slash( $value ) : $value;
  525. if ( update_post_meta( $product->get_id(), '_' . $key, $value ) ) {
  526. $this->updated_props[] = $key;
  527. }
  528. }
  529. }
  530. }
  531. if ( $this->update_downloads( $product, $force ) ) {
  532. $this->updated_props[] = 'downloads';
  533. }
  534. }
  535. /**
  536. * Handle updated meta props after updating meta data.
  537. *
  538. * @since 3.0.0
  539. * @param WC_Product $product Product Object.
  540. */
  541. protected function handle_updated_props( &$product ) {
  542. $price_is_synced = $product->is_type( array( 'variable', 'grouped' ) );
  543. if ( ! $price_is_synced ) {
  544. if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) {
  545. if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) {
  546. update_post_meta( $product->get_id(), '_sale_price', '' );
  547. $product->set_sale_price( '' );
  548. }
  549. }
  550. if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) {
  551. if ( $product->is_on_sale( 'edit' ) ) {
  552. update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) );
  553. $product->set_price( $product->get_sale_price( 'edit' ) );
  554. } else {
  555. update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) );
  556. $product->set_price( $product->get_regular_price( 'edit' ) );
  557. }
  558. }
  559. }
  560. if ( in_array( 'stock_quantity', $this->updated_props, true ) ) {
  561. if ( $product->is_type( 'variation' ) ) {
  562. do_action( 'woocommerce_variation_set_stock', $product );
  563. } else {
  564. do_action( 'woocommerce_product_set_stock', $product );
  565. }
  566. }
  567. if ( in_array( 'stock_status', $this->updated_props, true ) ) {
  568. if ( $product->is_type( 'variation' ) ) {
  569. do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
  570. } else {
  571. do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
  572. }
  573. }
  574. // Trigger action so 3rd parties can deal with updated props.
  575. do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props );
  576. // After handling, we can reset the props array.
  577. $this->updated_props = array();
  578. }
  579. /**
  580. * For all stored terms in all taxonomies, save them to the DB.
  581. *
  582. * @param WC_Product $product Product object.
  583. * @param bool $force Force update. Used during create.
  584. * @since 3.0.0
  585. */
  586. protected function update_terms( &$product, $force = false ) {
  587. $changes = $product->get_changes();
  588. if ( $force || array_key_exists( 'category_ids', $changes ) ) {
  589. $categories = $product->get_category_ids( 'edit' );
  590. if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) {
  591. $categories = array( get_option( 'default_product_cat', 0 ) );
  592. }
  593. wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false );
  594. }
  595. if ( $force || array_key_exists( 'tag_ids', $changes ) ) {
  596. wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false );
  597. }
  598. if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
  599. wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
  600. }
  601. }
  602. /**
  603. * Update visibility terms based on props.
  604. *
  605. * @since 3.0.0
  606. *
  607. * @param WC_Product $product Product object.
  608. * @param bool $force Force update. Used during create.
  609. */
  610. protected function update_visibility( &$product, $force = false ) {
  611. $changes = $product->get_changes();
  612. if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) {
  613. $terms = array();
  614. if ( $product->get_featured() ) {
  615. $terms[] = 'featured';
  616. }
  617. if ( 'outofstock' === $product->get_stock_status() ) {
  618. $terms[] = 'outofstock';
  619. }
  620. $rating = min( 5, round( $product->get_average_rating(), 0 ) );
  621. if ( $rating > 0 ) {
  622. $terms[] = 'rated-' . $rating;
  623. }
  624. switch ( $product->get_catalog_visibility() ) {
  625. case 'hidden':
  626. $terms[] = 'exclude-from-search';
  627. $terms[] = 'exclude-from-catalog';
  628. break;
  629. case 'catalog':
  630. $terms[] = 'exclude-from-search';
  631. break;
  632. case 'search':
  633. $terms[] = 'exclude-from-catalog';
  634. break;
  635. }
  636. if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) {
  637. delete_transient( 'wc_featured_products' );
  638. do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() );
  639. }
  640. }
  641. }
  642. /**
  643. * Update attributes which are a mix of terms and meta data.
  644. *
  645. * @param WC_Product $product Product object.
  646. * @param bool $force Force update. Used during create.
  647. * @since 3.0.0
  648. */
  649. protected function update_attributes( &$product, $force = false ) {
  650. $changes = $product->get_changes();
  651. if ( $force || array_key_exists( 'attributes', $changes ) ) {
  652. $attributes = $product->get_attributes();
  653. $meta_values = array();
  654. if ( $attributes ) {
  655. foreach ( $attributes as $attribute_key => $attribute ) {
  656. $value = '';
  657. delete_transient( 'wc_layered_nav_counts_' . $attribute_key );
  658. if ( is_null( $attribute ) ) {
  659. if ( taxonomy_exists( $attribute_key ) ) {
  660. // Handle attributes that have been unset.
  661. wp_set_object_terms( $product->get_id(), array(), $attribute_key );
  662. }
  663. continue;
  664. } elseif ( $attribute->is_taxonomy() ) {
  665. wp_set_object_terms( $product->get_id(), wp_list_pluck( $attribute->get_terms(), 'term_id' ), $attribute->get_name() );
  666. } else {
  667. $value = wc_implode_text_attributes( $attribute->get_options() );
  668. }
  669. // Store in format WC uses in meta.
  670. $meta_values[ $attribute_key ] = array(
  671. 'name' => $attribute->get_name(),
  672. 'value' => $value,
  673. 'position' => $attribute->get_position(),
  674. 'is_visible' => $attribute->get_visible() ? 1 : 0,
  675. 'is_variation' => $attribute->get_variation() ? 1 : 0,
  676. 'is_taxonomy' => $attribute->is_taxonomy() ? 1 : 0,
  677. );
  678. }
  679. }
  680. update_post_meta( $product->get_id(), '_product_attributes', $meta_values );
  681. }
  682. }
  683. /**
  684. * Update downloads.
  685. *
  686. * @since 3.0.0
  687. * @param WC_Product $product Product object.
  688. * @param bool $force Force update. Used during create.
  689. * @return bool If updated or not.
  690. */
  691. protected function update_downloads( &$product, $force = false ) {
  692. $changes = $product->get_changes();
  693. if ( $force || array_key_exists( 'downloads', $changes ) ) {
  694. $downloads = $product->get_downloads();
  695. $meta_values = array();
  696. if ( $downloads ) {
  697. foreach ( $downloads as $key => $download ) {
  698. // Store in format WC uses in meta.
  699. $meta_values[ $key ] = $download->get_data();
  700. }
  701. }
  702. if ( $product->is_type( 'variation' ) ) {
  703. do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads );
  704. } else {
  705. do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads );
  706. }
  707. return update_post_meta( $product->get_id(), '_downloadable_files', $meta_values );
  708. }
  709. return false;
  710. }
  711. /**
  712. * Make sure we store the product type and version (to track data changes).
  713. *
  714. * @param WC_Product $product Product object.
  715. * @since 3.0.0
  716. */
  717. protected function update_version_and_type( &$product ) {
  718. $old_type = WC_Product_Factory::get_product_type( $product->get_id() );
  719. $new_type = $product->get_type();
  720. wp_set_object_terms( $product->get_id(), $new_type, 'product_type' );
  721. update_post_meta( $product->get_id(), '_product_version', WC_VERSION );
  722. // Action for the transition.
  723. if ( $old_type !== $new_type ) {
  724. $this->updated_props[] = 'product_type';
  725. do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type );
  726. }
  727. }
  728. /**
  729. * Clear any caches.
  730. *
  731. * @param WC_Product $product Product object.
  732. * @since 3.0.0
  733. */
  734. protected function clear_caches( &$product ) {
  735. wc_delete_product_transients( $product->get_id() );
  736. WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_id() );
  737. }
  738. /*
  739. |--------------------------------------------------------------------------
  740. | wc-product-functions.php methods
  741. |--------------------------------------------------------------------------
  742. */
  743. /**
  744. * Returns an array of on sale products, as an array of objects with an
  745. * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id.
  746. *
  747. * @return array
  748. * @since 3.0.0
  749. */
  750. public function get_on_sale_products() {
  751. global $wpdb;
  752. $decimals = absint( wc_get_price_decimals() );
  753. $exclude_term_ids = array();
  754. $outofstock_join = '';
  755. $outofstock_where = '';
  756. $product_visibility_term_ids = wc_get_product_visibility_term_ids();
  757. if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
  758. $exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
  759. }
  760. if ( count( $exclude_term_ids ) ) {
  761. $outofstock_join = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id';
  762. $outofstock_where = ' AND exclude_join.object_id IS NULL';
  763. }
  764. return $wpdb->get_results(
  765. // phpcs:disable WordPress.WP.PreparedSQL.NotPrepared
  766. $wpdb->prepare(
  767. "SELECT post.ID as id, post.post_parent as parent_id FROM `$wpdb->posts` AS post
  768. LEFT JOIN `$wpdb->postmeta` AS meta ON post.ID = meta.post_id
  769. LEFT JOIN `$wpdb->postmeta` AS meta2 ON post.ID = meta2.post_id
  770. $outofstock_join
  771. WHERE post.post_type IN ( 'product', 'product_variation' )
  772. AND post.post_status = 'publish'
  773. AND meta.meta_key = '_sale_price'
  774. AND meta2.meta_key = '_price'
  775. AND CAST( meta.meta_value AS DECIMAL ) >= 0
  776. AND CAST( meta.meta_value AS CHAR ) != ''
  777. AND CAST( meta.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) )
  778. $outofstock_where
  779. GROUP BY post.ID",
  780. $decimals,
  781. $decimals
  782. )
  783. // phpcs:enable
  784. );
  785. }
  786. /**
  787. * Returns a list of product IDs ( id as key => parent as value) that are
  788. * featured. Uses get_posts instead of wc_get_products since we want
  789. * some extra meta queries and ALL products (posts_per_page = -1).
  790. *
  791. * @return array
  792. * @since 3.0.0
  793. */
  794. public function get_featured_product_ids() {
  795. $product_visibility_term_ids = wc_get_product_visibility_term_ids();
  796. return get_posts(
  797. array(
  798. 'post_type' => array( 'product', 'product_variation' ),
  799. 'posts_per_page' => -1,
  800. 'post_status' => 'publish',
  801. // phpcs:ignore WordPress.VIP.SlowDBQuery.slow_db_query_tax_query
  802. 'tax_query' => array(
  803. 'relation' => 'AND',
  804. array(
  805. 'taxonomy' => 'product_visibility',
  806. 'field' => 'term_taxonomy_id',
  807. 'terms' => array( $product_visibility_term_ids['featured'] ),
  808. ),
  809. array(
  810. 'taxonomy' => 'product_visibility',
  811. 'field' => 'term_taxonomy_id',
  812. 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ),
  813. 'operator' => 'NOT IN',
  814. ),
  815. ),
  816. 'fields' => 'id=>parent',
  817. )
  818. );
  819. }
  820. /**
  821. * Check if product sku is found for any other product IDs.
  822. *
  823. * @since 3.0.0
  824. * @param int $product_id Product ID.
  825. * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421.
  826. * @return bool
  827. */
  828. public function is_existing_sku( $product_id, $sku ) {
  829. global $wpdb;
  830. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  831. return $wpdb->get_var(
  832. $wpdb->prepare(
  833. "SELECT $wpdb->posts.ID
  834. FROM $wpdb->posts
  835. LEFT JOIN $wpdb->postmeta ON ( $wpdb->posts.ID = $wpdb->postmeta.post_id )
  836. WHERE $wpdb->posts.post_type IN ( 'product', 'product_variation' )
  837. AND $wpdb->posts.post_status != 'trash'
  838. AND $wpdb->postmeta.meta_key = '_sku' AND $wpdb->postmeta.meta_value = %s
  839. AND $wpdb->postmeta.post_id <> %d
  840. LIMIT 1",
  841. wp_slash( $sku ), $product_id
  842. )
  843. );
  844. }
  845. /**
  846. * Return product ID based on SKU.
  847. *
  848. * @since 3.0.0
  849. * @param string $sku Product SKU.
  850. * @return int
  851. */
  852. public function get_product_id_by_sku( $sku ) {
  853. global $wpdb;
  854. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  855. $id = $wpdb->get_var(
  856. $wpdb->prepare(
  857. "SELECT posts.ID
  858. FROM $wpdb->posts AS posts
  859. LEFT JOIN $wpdb->postmeta AS postmeta ON ( posts.ID = postmeta.post_id )
  860. WHERE posts.post_type IN ( 'product', 'product_variation' )
  861. AND posts.post_status != 'trash'
  862. AND postmeta.meta_key = '_sku'
  863. AND postmeta.meta_value = %s
  864. LIMIT 1",
  865. $sku
  866. )
  867. );
  868. return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku );
  869. }
  870. /**
  871. * Returns an array of IDs of products that have sales starting soon.
  872. *
  873. * @since 3.0.0
  874. * @return array
  875. */
  876. public function get_starting_sales() {
  877. global $wpdb;
  878. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  879. return $wpdb->get_col(
  880. $wpdb->prepare(
  881. "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
  882. LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
  883. LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
  884. WHERE postmeta.meta_key = '_sale_price_dates_from'
  885. AND postmeta_2.meta_key = '_price'
  886. AND postmeta_3.meta_key = '_sale_price'
  887. AND postmeta.meta_value > 0
  888. AND postmeta.meta_value < %s
  889. AND postmeta_2.meta_value != postmeta_3.meta_value",
  890. current_time( 'timestamp', true )
  891. )
  892. );
  893. }
  894. /**
  895. * Returns an array of IDs of products that have sales which are due to end.
  896. *
  897. * @since 3.0.0
  898. * @return array
  899. */
  900. public function get_ending_sales() {
  901. global $wpdb;
  902. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  903. return $wpdb->get_col(
  904. $wpdb->prepare(
  905. "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
  906. LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
  907. LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
  908. WHERE postmeta.meta_key = '_sale_price_dates_to'
  909. AND postmeta_2.meta_key = '_price'
  910. AND postmeta_3.meta_key = '_regular_price'
  911. AND postmeta.meta_value > 0
  912. AND postmeta.meta_value < %s
  913. AND postmeta_2.meta_value != postmeta_3.meta_value",
  914. current_time( 'timestamp', true )
  915. )
  916. );
  917. }
  918. /**
  919. * Find a matching (enabled) variation within a variable product.
  920. *
  921. * @since 3.0.0
  922. * @param WC_Product $product Variable product.
  923. * @param array $match_attributes Array of attributes we want to try to match.
  924. * @return int Matching variation ID or 0.
  925. */
  926. public function find_matching_product_variation( $product, $match_attributes = array() ) {
  927. $query_args = array(
  928. 'post_parent' => $product->get_id(),
  929. 'post_type' => 'product_variation',
  930. 'orderby' => 'menu_order',
  931. 'order' => 'ASC',
  932. 'fields' => 'ids',
  933. 'post_status' => 'publish',
  934. 'numberposts' => 1,
  935. 'meta_query' => array(), // phpcs:ignore WordPress.VIP.SlowDBQuery.slow_db_query_meta_query
  936. );
  937. // Allow large queries in case user has many variations or attributes.
  938. $GLOBALS['wpdb']->query( 'SET SESSION SQL_BIG_SELECTS=1' );
  939. foreach ( $product->get_attributes() as $attribute ) {
  940. if ( ! $attribute->get_variation() ) {
  941. continue;
  942. }
  943. $attribute_field_name = 'attribute_' . sanitize_title( $attribute->get_name() );
  944. if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) {
  945. return 0;
  946. }
  947. // Note not wc_clean here to prevent removal of entities.
  948. $value = $match_attributes[ $attribute_field_name ];
  949. $query_args['meta_query'][] = array(
  950. 'relation' => 'OR',
  951. array(
  952. 'key' => $attribute_field_name,
  953. 'value' => array( '', $value ),
  954. 'compare' => 'IN',
  955. ),
  956. array(
  957. 'key' => $attribute_field_name,
  958. 'compare' => 'NOT EXISTS',
  959. ),
  960. );
  961. }
  962. $variations = get_posts( $query_args );
  963. if ( $variations && ! is_wp_error( $variations ) ) {
  964. return current( $variations );
  965. } elseif ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
  966. /**
  967. * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
  968. * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same.
  969. */
  970. return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) );
  971. }
  972. return 0;
  973. }
  974. /**
  975. * Make sure all variations have a sort order set so they can be reordered correctly.
  976. *
  977. * @param int $parent_id Product ID.
  978. */
  979. public function sort_all_product_variations( $parent_id ) {
  980. global $wpdb;
  981. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  982. $ids = $wpdb->get_col(
  983. $wpdb->prepare(
  984. "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC",
  985. $parent_id
  986. )
  987. );
  988. $index = 1;
  989. foreach ( $ids as $id ) {
  990. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  991. $wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) );
  992. }
  993. }
  994. /**
  995. * Return a list of related products (using data like categories and IDs).
  996. *
  997. * @since 3.0.0
  998. * @param array $cats_array List of categories IDs.
  999. * @param array $tags_array List of tags IDs.
  1000. * @param array $exclude_ids Excluded IDs.
  1001. * @param int $limit Limit of results.
  1002. * @param int $product_id Product ID.
  1003. * @return array
  1004. */
  1005. public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) {
  1006. global $wpdb;
  1007. $args = array(
  1008. 'categories' => $cats_array,
  1009. 'tags' => $tags_array,
  1010. 'exclude_ids' => $exclude_ids,
  1011. 'limit' => $limit + 10,
  1012. );
  1013. $related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args );
  1014. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.WP.PreparedSQL.NotPrepared
  1015. return $wpdb->get_col( implode( ' ', $related_product_query ) );
  1016. }
  1017. /**
  1018. * Builds the related posts query.
  1019. *
  1020. * @since 3.0.0
  1021. *
  1022. * @param array $cats_array List of categories IDs.
  1023. * @param array $tags_array List of tags IDs.
  1024. * @param array $exclude_ids Excluded IDs.
  1025. * @param int $limit Limit of results.
  1026. *
  1027. * @return array
  1028. */
  1029. public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
  1030. global $wpdb;
  1031. $include_term_ids = array_merge( $cats_array, $tags_array );
  1032. $exclude_term_ids = array();
  1033. $product_visibility_term_ids = wc_get_product_visibility_term_ids();
  1034. if ( $product_visibility_term_ids['exclude-from-catalog'] ) {
  1035. $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog'];
  1036. }
  1037. if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
  1038. $exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
  1039. }
  1040. $query = array(
  1041. 'fields' => "
  1042. SELECT DISTINCT ID FROM {$wpdb->posts} p
  1043. ",
  1044. 'join' => '',
  1045. 'where' => "
  1046. WHERE 1=1
  1047. AND p.post_status = 'publish'
  1048. AND p.post_type = 'product'
  1049. ",
  1050. 'limits' => '
  1051. LIMIT ' . absint( $limit ) . '
  1052. ',
  1053. );
  1054. if ( count( $exclude_term_ids ) ) {
  1055. $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID';
  1056. $query['where'] .= ' AND exclude_join.object_id IS NULL';
  1057. }
  1058. if ( count( $include_term_ids ) ) {
  1059. $query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID';
  1060. }
  1061. if ( count( $exclude_ids ) ) {
  1062. $query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )';
  1063. }
  1064. return $query;
  1065. }
  1066. /**
  1067. * Update a product's stock amount directly.
  1068. *
  1069. * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
  1070. *
  1071. * @since 3.0.0 this supports set, increase and decrease.
  1072. * @param int $product_id_with_stock Product ID.
  1073. * @param int|null $stock_quantity Stock quantity.
  1074. * @param string $operation Set, increase and decrease.
  1075. */
  1076. public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) {
  1077. global $wpdb;
  1078. add_post_meta( $product_id_with_stock, '_stock', 0, true );
  1079. // Update stock in DB directly.
  1080. switch ( $operation ) {
  1081. case 'increase':
  1082. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  1083. $wpdb->query(
  1084. $wpdb->prepare(
  1085. "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='_stock'", $stock_quantity, $product_id_with_stock
  1086. )
  1087. );
  1088. break;
  1089. case 'decrease':
  1090. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  1091. $wpdb->query(
  1092. $wpdb->prepare(
  1093. "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='_stock'", $stock_quantity, $product_id_with_stock
  1094. )
  1095. );
  1096. break;
  1097. default:
  1098. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  1099. $wpdb->query(
  1100. $wpdb->prepare(
  1101. "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", $stock_quantity, $product_id_with_stock
  1102. )
  1103. );
  1104. break;
  1105. }
  1106. wp_cache_delete( $product_id_with_stock, 'post_meta' );
  1107. }
  1108. /**
  1109. * Update a product's sale count directly.
  1110. *
  1111. * Uses queries rather than update_post_meta so we can do this in one query for performance.
  1112. *
  1113. * @since 3.0.0 this supports set, increase and decrease.
  1114. * @param int $product_id Product ID.
  1115. * @param int|null $quantity Quantity.
  1116. * @param string $operation set, increase and decrease.
  1117. */
  1118. public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) {
  1119. global $wpdb;
  1120. add_post_meta( $product_id, 'total_sales', 0, true );
  1121. // Update stock in DB directly.
  1122. switch ( $operation ) {
  1123. case 'increase':
  1124. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  1125. $wpdb->query(
  1126. $wpdb->prepare(
  1127. "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id
  1128. )
  1129. );
  1130. break;
  1131. case 'decrease':
  1132. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  1133. $wpdb->query(
  1134. $wpdb->prepare(
  1135. "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id
  1136. )
  1137. );
  1138. break;
  1139. default:
  1140. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  1141. $wpdb->query(
  1142. $wpdb->prepare(
  1143. "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id
  1144. )
  1145. );
  1146. break;
  1147. }
  1148. wp_cache_delete( $product_id, 'post_meta' );
  1149. }
  1150. /**
  1151. * Update a products average rating meta.
  1152. *
  1153. * @since 3.0.0
  1154. * @param WC_Product $product Product object.
  1155. */
  1156. public function update_average_rating( $product ) {
  1157. update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) );
  1158. self::update_visibility( $product, true );
  1159. }
  1160. /**
  1161. * Update a products review count meta.
  1162. *
  1163. * @since 3.0.0
  1164. * @param WC_Product $product Product object.
  1165. */
  1166. public function update_review_count( $product ) {
  1167. update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) );
  1168. }
  1169. /**
  1170. * Update a products rating counts.
  1171. *
  1172. * @since 3.0.0
  1173. * @param WC_Product $product Product object.
  1174. */
  1175. public function update_rating_counts( $product ) {
  1176. update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) );
  1177. }
  1178. /**
  1179. * Get shipping class ID by slug.
  1180. *
  1181. * @since 3.0.0
  1182. * @param string $slug Product shipping class slug.
  1183. * @return int|false
  1184. */
  1185. public function get_shipping_class_id_by_slug( $slug ) {
  1186. $shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' );
  1187. if ( $shipping_class_term ) {
  1188. return $shipping_class_term->term_id;
  1189. } else {
  1190. return false;
  1191. }
  1192. }
  1193. /**
  1194. * Returns an array of products.
  1195. *
  1196. * @param array $args Args to pass to WC_Product_Query().
  1197. * @return array|object
  1198. * @see wc_get_products
  1199. */
  1200. public function get_products( $args = array() ) {
  1201. $query = new WC_Product_Query( $args );
  1202. return $query->get_products();
  1203. }
  1204. /**
  1205. * Search product data for a term and return ids.
  1206. *
  1207. * @param string $term Search term.
  1208. * @param string $type Type of product.
  1209. * @param bool $include_variations Include variations in search or not.
  1210. * @param bool $all_statuses Should we search all statuses or limit to published.
  1211. * @return array of ids
  1212. */
  1213. public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false ) {
  1214. global $wpdb;
  1215. $post_types = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' );
  1216. $post_statuses = current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' );
  1217. $type_join = '';
  1218. $type_where = '';
  1219. $status_where = '';
  1220. $term = wc_strtolower( $term );
  1221. // See if search term contains OR keywords.
  1222. if ( strstr( $term, ' or ' ) ) {
  1223. $term_groups = explode( ' or ', $term );
  1224. } else {
  1225. $term_groups = array( $term );
  1226. }
  1227. $search_where = '';
  1228. $search_queries = array();
  1229. foreach ( $term_groups as $term_group ) {
  1230. // Parse search terms.
  1231. if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) {
  1232. $search_terms = $this->get_valid_search_terms( $matches[0] );
  1233. $count = count( $search_terms );
  1234. // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence.
  1235. if ( 9 < $count || 0 === $count ) {
  1236. $search_terms = array( $term_group );
  1237. }
  1238. } else {
  1239. $search_terms = array( $term_group );
  1240. }
  1241. $term_group_query = '';
  1242. $searchand = '';
  1243. foreach ( $search_terms as $search_term ) {
  1244. $like = '%' . $wpdb->esc_like( $search_term ) . '%';
  1245. $term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( postmeta.meta_key = '_sku' AND postmeta.meta_value LIKE %s ) )", $like, $like, $like, $like ); // @codingStandardsIgnoreLine.
  1246. $searchand = ' AND ';
  1247. }
  1248. if ( $term_group_query ) {
  1249. $search_queries[] = $term_group_query;
  1250. }
  1251. }
  1252. if ( $search_queries ) {
  1253. $search_where = 'AND (' . implode( ') OR (', $search_queries ) . ')';
  1254. }
  1255. if ( $type && in_array( $type, array( 'virtual', 'downloadable' ), true ) ) {
  1256. $type_join = " LEFT JOIN {$wpdb->postmeta} postmeta_type ON posts.ID = postmeta_type.post_id ";
  1257. $type_where = " AND ( postmeta_type.meta_key = '_{$type}' AND postmeta_type.meta_value = 'yes' ) ";
  1258. }
  1259. if ( ! $all_statuses ) {
  1260. $status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') ";
  1261. }
  1262. // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
  1263. $search_results = $wpdb->get_results(
  1264. // phpcs:disable
  1265. "SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts
  1266. LEFT JOIN {$wpdb->postmeta} postmeta ON posts.ID = postmeta.post_id
  1267. $type_join
  1268. WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "')
  1269. $search_where
  1270. $status_where
  1271. $type_where
  1272. ORDER BY posts.post_parent ASC, posts.post_title ASC"
  1273. // phpcs:enable
  1274. );
  1275. $product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) );
  1276. if ( is_numeric( $term ) ) {
  1277. $post_id = absint( $term );
  1278. $post_type = get_post_type( $post_id );
  1279. if ( 'product_variation' === $post_type && $include_variations ) {
  1280. $product_ids[] = $post_id;
  1281. } elseif ( 'product' === $post_type ) {
  1282. $product_ids[] = $post_id;
  1283. }
  1284. $product_ids[] = wp_get_post_parent_id( $post_id );
  1285. }
  1286. return wp_parse_id_list( $product_ids );
  1287. }
  1288. /**
  1289. * Get the product type based on product ID.
  1290. *
  1291. * @since 3.0.0
  1292. * @param int $product_id Product ID.
  1293. * @return bool|string
  1294. */
  1295. public function get_product_type( $product_id ) {
  1296. $post_type = get_post_type( $product_id );
  1297. if ( 'product_variation' === $post_type ) {
  1298. return 'variation';
  1299. } elseif ( 'product' === $post_type ) {
  1300. $terms = get_the_terms( $product_id, 'product_type' );
  1301. return ! empty( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple';
  1302. } else {
  1303. return false;
  1304. }
  1305. }
  1306. /**
  1307. * Add ability to get products by 'reviews_allowed' in WC_Product_Query.
  1308. *
  1309. * @since 3.2.0
  1310. * @param string $where Where clause.
  1311. * @param WP_Query $wp_query WP_Query instance.
  1312. * @return string
  1313. */
  1314. public function reviews_allowed_query_where( $where, $wp_query ) {
  1315. global $wpdb;
  1316. if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) {
  1317. if ( $wp_query->query_vars['reviews_allowed'] ) {
  1318. $where .= " AND $wpdb->posts.comment_status = 'open'";
  1319. } else {
  1320. $where .= " AND $wpdb->posts.comment_status = 'closed'";
  1321. }
  1322. }
  1323. return $where;
  1324. }
  1325. /**
  1326. * Get valid WP_Query args from a WC_Product_Query's query variables.
  1327. *
  1328. * @since 3.2.0
  1329. * @param array $query_vars Query vars from a WC_Product_Query.
  1330. * @return array
  1331. */
  1332. protected function get_wp_query_args( $query_vars ) {
  1333. // Map query vars to ones that get_wp_query_args or WP_Query recognize.
  1334. $key_mapping = array(
  1335. 'status' => 'post_status',
  1336. 'page' => 'paged',
  1337. 'include' => 'post__in',
  1338. 'stock_quantity' => 'stock',
  1339. 'average_rating' => 'wc_average_rating',
  1340. 'review_count' => 'wc_review_count',
  1341. );
  1342. foreach ( $key_mapping as $query_key => $db_key ) {
  1343. if ( isset( $query_vars[ $query_key ] ) ) {
  1344. $query_vars[ $db_key ] = $query_vars[ $query_key ];
  1345. unset( $query_vars[ $query_key ] );
  1346. }
  1347. }
  1348. // Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'.
  1349. $boolean_queries = array(
  1350. 'virtual',
  1351. 'downloadable',
  1352. 'sold_individually',
  1353. 'manage_stock',
  1354. );
  1355. foreach ( $boolean_queries as $boolean_query ) {
  1356. if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) {
  1357. $query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no';
  1358. }
  1359. }
  1360. // These queries cannot be auto-generated so we have to remove them and build them manually.
  1361. $manual_queries = array(
  1362. 'sku' => '',
  1363. 'featured' => '',
  1364. 'visibility' => '',
  1365. );
  1366. foreach ( $manual_queries as $key => $manual_query ) {
  1367. if ( isset( $query_vars[ $key ] ) ) {
  1368. $manual_queries[ $key ] = $query_vars[ $key ];
  1369. unset( $query_vars[ $key ] );
  1370. }
  1371. }
  1372. $wp_query_args = parent::get_wp_query_args( $query_vars );
  1373. if ( ! isset( $wp_query_args['date_query'] ) ) {
  1374. $wp_query_args['date_query'] = array();
  1375. }
  1376. if ( ! isset( $wp_query_args['meta_query'] ) ) {
  1377. // phpcs:ignore WordPress.VIP.SlowDBQuery.slow_db_query_meta_query
  1378. $wp_query_args['meta_query'] = array();
  1379. }
  1380. // Handle product types.
  1381. if ( 'variation' === $query_vars['type'] ) {
  1382. $wp_query_args['post_type'] = 'product_variation';
  1383. } elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) {
  1384. $wp_query_args['post_type'] = array( 'product_variation', 'product' );
  1385. $wp_query_args['tax_query'][] = array(
  1386. 'relation' => 'OR',
  1387. array(
  1388. 'taxonomy' => 'product_type',
  1389. 'field' => 'slug',
  1390. 'terms' => $query_vars['type'],
  1391. ),
  1392. array(
  1393. 'taxonomy' => 'product_type',
  1394. 'field' => 'id',
  1395. 'operator' => 'NOT EXISTS',
  1396. ),
  1397. );
  1398. } else {
  1399. $wp_query_args['post_type'] = 'product';
  1400. $wp_query_args['tax_query'][] = array(
  1401. 'taxonomy' => 'product_type',
  1402. 'field' => 'slug',
  1403. 'terms' => $query_vars['type'],
  1404. );
  1405. }
  1406. // Handle product categories.
  1407. if ( ! empty( $query_vars['category'] ) ) {
  1408. $wp_query_args['tax_query'][] = array(
  1409. 'taxonomy' => 'product_cat',
  1410. 'field' => 'slug',
  1411. 'terms' => $query_vars['category'],
  1412. );
  1413. }
  1414. // Handle product tags.
  1415. if ( ! empty( $query_vars['tag'] ) ) {
  1416. unset( $wp_query_args['tag'] );
  1417. $wp_query_args['tax_query'][] = array(
  1418. 'taxonomy' => 'product_tag',
  1419. 'field' => 'slug',
  1420. 'terms' => $query_vars['tag'],
  1421. );
  1422. }
  1423. // Handle shipping classes.
  1424. if ( ! empty( $query_vars['shipping_class'] ) ) {
  1425. $wp_query_args['tax_query'][] = array(
  1426. 'taxonomy' => 'product_shipping_class',
  1427. 'field' => 'slug',
  1428. 'terms' => $query_vars['shipping_class'],
  1429. );
  1430. }
  1431. // Handle total_sales.
  1432. // This query doesn't get auto-generated since the meta key doesn't have the underscore prefix.
  1433. if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) {
  1434. $wp_query_args['meta_query'][] = array(
  1435. 'key' => 'total_sales',
  1436. 'value' => absint( $query_vars['total_sales'] ),
  1437. 'compare' => '=',
  1438. );
  1439. }
  1440. // Handle SKU.
  1441. if ( $manual_queries['sku'] ) {
  1442. $wp_query_args['meta_query'][] = array(
  1443. 'key' => '_sku',
  1444. 'value' => $manual_queries['sku'],
  1445. 'compare' => 'LIKE',
  1446. );
  1447. }
  1448. // Handle featured.
  1449. if ( '' !== $manual_queries['featured'] ) {
  1450. $product_visibility_term_ids = wc_get_product_visibility_term_ids();
  1451. if ( $manual_queries['featured'] ) {
  1452. $wp_query_args['tax_query'][] = array(
  1453. 'taxonomy' => 'product_visibility',
  1454. 'field' => 'term_taxonomy_id',
  1455. 'terms' => array( $product_visibility_term_ids['featured'] ),
  1456. );
  1457. $wp_query_args['tax_query'][] = array(
  1458. 'taxonomy' => 'product_visibility',
  1459. 'field' => 'term_taxonomy_id',
  1460. 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ),
  1461. 'operator' => 'NOT IN',
  1462. );
  1463. } else {
  1464. $wp_query_args['tax_query'][] = array(
  1465. 'taxonomy' => 'product_visibility',
  1466. 'field' => 'term_taxonomy_id',
  1467. 'terms' => array( $product_visibility_term_ids['featured'] ),
  1468. 'operator' => 'NOT IN',
  1469. );
  1470. }
  1471. }
  1472. // Handle visibility.
  1473. if ( $manual_queries['visibility'] ) {
  1474. switch ( $manual_queries['visibility'] ) {
  1475. case 'search':
  1476. $wp_query_args['tax_query'][] = array(
  1477. 'taxonomy' => 'product_visibility',
  1478. 'field' => 'slug',
  1479. 'terms' => array( 'exclude-from-search' ),
  1480. 'operator' => 'NOT IN',
  1481. );
  1482. break;
  1483. case 'catalog':
  1484. $wp_query_args['tax_query'][] = array(
  1485. 'taxonomy' => 'product_visibility',
  1486. 'field' => 'slug',
  1487. 'terms' => array( 'exclude-from-catalog' ),
  1488. 'operator' => 'NOT IN',
  1489. );
  1490. break;
  1491. case 'visible':
  1492. $wp_query_args['tax_query'][] = array(
  1493. 'taxonomy' => 'product_visibility',
  1494. 'field' => 'slug',
  1495. 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ),
  1496. 'operator' => 'NOT IN',
  1497. );
  1498. break;
  1499. case 'hidden':
  1500. $wp_query_args['tax_query'][] = array(
  1501. 'taxonomy' => 'product_visibility',
  1502. 'field' => 'slug',
  1503. 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ),
  1504. 'operator' => 'AND',
  1505. );
  1506. break;
  1507. }
  1508. }
  1509. // Handle date queries.
  1510. $date_queries = array(
  1511. 'date_created' => 'post_date',
  1512. 'date_modified' => 'post_modified',
  1513. 'date_on_sale_from' => '_sale_price_dates_from',
  1514. 'date_on_sale_to' => '_sale_price_dates_to',
  1515. );
  1516. foreach ( $date_queries as $query_var_key => $db_key ) {
  1517. if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {
  1518. // Remove any existing meta queries for the same keys to prevent conflicts.
  1519. $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
  1520. foreach ( $existing_queries as $query_index => $query_contents ) {
  1521. unset( $wp_query_args['meta_query'][ $query_index ] );
  1522. }
  1523. $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
  1524. }
  1525. }
  1526. // Handle paginate.
  1527. if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
  1528. $wp_query_args['no_found_rows'] = true;
  1529. }
  1530. // Handle reviews_allowed.
  1531. if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) {
  1532. add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 );
  1533. }
  1534. return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this );
  1535. }
  1536. /**
  1537. * Query for Products matching specific criteria.
  1538. *
  1539. * @since 3.2.0
  1540. *
  1541. * @param array $query_vars Query vars from a WC_Product_Query.
  1542. *
  1543. * @return array|object
  1544. */
  1545. public function query( $query_vars ) {
  1546. $args = $this->get_wp_query_args( $query_vars );
  1547. if ( ! empty( $args['errors'] ) ) {
  1548. $query = (object) array(
  1549. 'posts' => array(),
  1550. 'found_posts' => 0,
  1551. 'max_num_pages' => 0,
  1552. );
  1553. } else {
  1554. $query = new WP_Query( $args );
  1555. }
  1556. if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) {
  1557. // Prime caches before grabbing objects.
  1558. update_post_caches( $query->posts, array( 'product', 'product_variation' ) );
  1559. }
  1560. $products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) );
  1561. if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
  1562. return (object) array(
  1563. 'products' => $products,
  1564. 'total' => $query->found_posts,
  1565. 'max_num_pages' => $query->max_num_pages,
  1566. );
  1567. }
  1568. return $products;
  1569. }
  1570. }