class-wc-product-csv-exporter.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. <?php
  2. /**
  3. * Handles product CSV export.
  4. *
  5. * @package WooCommerce/Export
  6. * @version 3.1.0
  7. */
  8. if ( ! defined( 'ABSPATH' ) ) {
  9. exit;
  10. }
  11. /**
  12. * Include dependencies.
  13. */
  14. if ( ! class_exists( 'WC_CSV_Batch_Exporter', false ) ) {
  15. include_once WC_ABSPATH . 'includes/export/abstract-wc-csv-batch-exporter.php';
  16. }
  17. /**
  18. * WC_Product_CSV_Exporter Class.
  19. */
  20. class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter {
  21. /**
  22. * Type of export used in filter names.
  23. *
  24. * @var string
  25. */
  26. protected $export_type = 'product';
  27. /**
  28. * Should meta be exported?
  29. *
  30. * @var boolean
  31. */
  32. protected $enable_meta_export = false;
  33. /**
  34. * Which product types are being exported.
  35. *
  36. * @var array
  37. */
  38. protected $product_types_to_export = array();
  39. /**
  40. * Constructor.
  41. */
  42. public function __construct() {
  43. parent::__construct();
  44. $this->set_product_types_to_export( array_merge( array_keys( wc_get_product_types() ), array( 'variation' ) ) );
  45. }
  46. /**
  47. * Should meta be exported?
  48. *
  49. * @since 3.1.0
  50. * @param bool $enable_meta_export Should meta be exported.
  51. */
  52. public function enable_meta_export( $enable_meta_export ) {
  53. $this->enable_meta_export = (bool) $enable_meta_export;
  54. }
  55. /**
  56. * Product types to export.
  57. *
  58. * @since 3.1.0
  59. * @param array $product_types_to_export List of types to export.
  60. */
  61. public function set_product_types_to_export( $product_types_to_export ) {
  62. $this->product_types_to_export = array_map( 'wc_clean', $product_types_to_export );
  63. }
  64. /**
  65. * Return an array of columns to export.
  66. *
  67. * @since 3.1.0
  68. * @return array
  69. */
  70. public function get_default_column_names() {
  71. return apply_filters( "woocommerce_product_export_{$this->export_type}_default_columns", array(
  72. 'id' => __( 'ID', 'woocommerce' ),
  73. 'type' => __( 'Type', 'woocommerce' ),
  74. 'sku' => __( 'SKU', 'woocommerce' ),
  75. 'name' => __( 'Name', 'woocommerce' ),
  76. 'published' => __( 'Published', 'woocommerce' ),
  77. 'featured' => __( 'Is featured?', 'woocommerce' ),
  78. 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ),
  79. 'short_description' => __( 'Short description', 'woocommerce' ),
  80. 'description' => __( 'Description', 'woocommerce' ),
  81. 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ),
  82. 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ),
  83. 'tax_status' => __( 'Tax status', 'woocommerce' ),
  84. 'tax_class' => __( 'Tax class', 'woocommerce' ),
  85. 'stock_status' => __( 'In stock?', 'woocommerce' ),
  86. 'stock' => __( 'Stock', 'woocommerce' ),
  87. 'backorders' => __( 'Backorders allowed?', 'woocommerce' ),
  88. 'sold_individually' => __( 'Sold individually?', 'woocommerce' ),
  89. /* translators: %s: weight */
  90. 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ),
  91. /* translators: %s: length */
  92. 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ),
  93. /* translators: %s: width */
  94. 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ),
  95. /* translators: %s: Height */
  96. 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ),
  97. 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ),
  98. 'purchase_note' => __( 'Purchase note', 'woocommerce' ),
  99. 'sale_price' => __( 'Sale price', 'woocommerce' ),
  100. 'regular_price' => __( 'Regular price', 'woocommerce' ),
  101. 'category_ids' => __( 'Categories', 'woocommerce' ),
  102. 'tag_ids' => __( 'Tags', 'woocommerce' ),
  103. 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ),
  104. 'images' => __( 'Images', 'woocommerce' ),
  105. 'download_limit' => __( 'Download limit', 'woocommerce' ),
  106. 'download_expiry' => __( 'Download expiry days', 'woocommerce' ),
  107. 'parent_id' => __( 'Parent', 'woocommerce' ),
  108. 'grouped_products' => __( 'Grouped products', 'woocommerce' ),
  109. 'upsell_ids' => __( 'Upsells', 'woocommerce' ),
  110. 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ),
  111. 'product_url' => __( 'External URL', 'woocommerce' ),
  112. 'button_text' => __( 'Button text', 'woocommerce' ),
  113. 'menu_order' => __( 'Position', 'woocommerce' ),
  114. ) );
  115. }
  116. /**
  117. * Prepare data for export.
  118. *
  119. * @since 3.1.0
  120. */
  121. public function prepare_data_to_export() {
  122. $columns = $this->get_column_names();
  123. $args = apply_filters( "woocommerce_product_export_{$this->export_type}_query_args", array(
  124. 'status' => array( 'private', 'publish', 'draft', 'future', 'pending' ),
  125. 'type' => $this->product_types_to_export,
  126. 'limit' => $this->get_limit(),
  127. 'page' => $this->get_page(),
  128. 'orderby' => array(
  129. 'ID' => 'ASC',
  130. ),
  131. 'return' => 'objects',
  132. 'paginate' => true,
  133. ) );
  134. $products = wc_get_products( $args );
  135. $this->total_rows = $products->total;
  136. $this->row_data = array();
  137. foreach ( $products->products as $product ) {
  138. $row = array();
  139. foreach ( $columns as $column_id => $column_name ) {
  140. $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id;
  141. $value = '';
  142. // Skip some columns if dynamically handled later or if we're being selective.
  143. if ( in_array( $column_id, array( 'downloads', 'attributes', 'meta' ), true ) || ! $this->is_column_exporting( $column_id ) ) {
  144. continue;
  145. }
  146. if ( has_filter( "woocommerce_product_export_{$this->export_type}_column_{$column_id}" ) ) {
  147. // Filter for 3rd parties.
  148. $value = apply_filters( "woocommerce_product_export_{$this->export_type}_column_{$column_id}", '', $product, $column_id );
  149. } elseif ( is_callable( array( $this, "get_column_value_{$column_id}" ) ) ) {
  150. // Handle special columns which don't map 1:1 to product data.
  151. $value = $this->{"get_column_value_{$column_id}"}( $product );
  152. } elseif ( is_callable( array( $product, "get_{$column_id}" ) ) ) {
  153. // Default and custom handling.
  154. $value = $product->{"get_{$column_id}"}( 'edit' );
  155. }
  156. $row[ $column_id ] = $value;
  157. }
  158. $this->prepare_downloads_for_export( $product, $row );
  159. $this->prepare_attributes_for_export( $product, $row );
  160. $this->prepare_meta_for_export( $product, $row );
  161. $this->row_data[] = apply_filters( 'woocommerce_product_export_row_data', $row, $product );
  162. }
  163. }
  164. /**
  165. * Get published value.
  166. *
  167. * @since 3.1.0
  168. * @param WC_Product $product Product being exported.
  169. * @return int
  170. */
  171. protected function get_column_value_published( $product ) {
  172. $statuses = array(
  173. 'draft' => -1,
  174. 'private' => 0,
  175. 'publish' => 1,
  176. );
  177. $status = $product->get_status( 'edit' );
  178. return isset( $statuses[ $status ] ) ? $statuses[ $status ] : -1;
  179. }
  180. /**
  181. * Get formatted sale price.
  182. *
  183. * @param WC_Product $product Product being exported.
  184. * @return string
  185. */
  186. protected function get_column_value_sale_price( $product ) {
  187. return wc_format_localized_price( $product->get_sale_price( 'view' ) );
  188. }
  189. /**
  190. * Get formatted regular price.
  191. *
  192. * @param WC_Product $product Product being exported.
  193. * @return string
  194. */
  195. protected function get_column_value_regular_price( $product ) {
  196. return wc_format_localized_price( $product->get_regular_price() );
  197. }
  198. /**
  199. * Get product_cat value.
  200. *
  201. * @since 3.1.0
  202. * @param WC_Product $product Product being exported.
  203. * @return string
  204. */
  205. protected function get_column_value_category_ids( $product ) {
  206. $term_ids = $product->get_category_ids( 'edit' );
  207. return $this->format_term_ids( $term_ids, 'product_cat' );
  208. }
  209. /**
  210. * Get product_tag value.
  211. *
  212. * @since 3.1.0
  213. * @param WC_Product $product Product being exported.
  214. * @return string
  215. */
  216. protected function get_column_value_tag_ids( $product ) {
  217. $term_ids = $product->get_tag_ids( 'edit' );
  218. return $this->format_term_ids( $term_ids, 'product_tag' );
  219. }
  220. /**
  221. * Get product_shipping_class value.
  222. *
  223. * @since 3.1.0
  224. * @param WC_Product $product Product being exported.
  225. * @return string
  226. */
  227. protected function get_column_value_shipping_class_id( $product ) {
  228. $term_ids = $product->get_shipping_class_id( 'edit' );
  229. return $this->format_term_ids( $term_ids, 'product_shipping_class' );
  230. }
  231. /**
  232. * Get images value.
  233. *
  234. * @since 3.1.0
  235. * @param WC_Product $product Product being exported.
  236. * @return string
  237. */
  238. protected function get_column_value_images( $product ) {
  239. $image_ids = array_merge( array( $product->get_image_id( 'edit' ) ), $product->get_gallery_image_ids( 'edit' ) );
  240. $images = array();
  241. foreach ( $image_ids as $image_id ) {
  242. $image = wp_get_attachment_image_src( $image_id, 'full' );
  243. if ( $image ) {
  244. $images[] = $image[0];
  245. }
  246. }
  247. return $this->implode_values( $images );
  248. }
  249. /**
  250. * Prepare linked products for export.
  251. *
  252. * @since 3.1.0
  253. * @param int[] $linked_products Array of linked product ids.
  254. * @return string
  255. */
  256. protected function prepare_linked_products_for_export( $linked_products ) {
  257. $product_list = array();
  258. foreach ( $linked_products as $linked_product ) {
  259. if ( $linked_product->get_sku() ) {
  260. $product_list[] = $linked_product->get_sku();
  261. } else {
  262. $product_list[] = 'id:' . $linked_product->get_id();
  263. }
  264. }
  265. return $this->implode_values( $product_list );
  266. }
  267. /**
  268. * Get cross_sell_ids value.
  269. *
  270. * @since 3.1.0
  271. * @param WC_Product $product Product being exported.
  272. * @return string
  273. */
  274. protected function get_column_value_cross_sell_ids( $product ) {
  275. return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_cross_sell_ids( 'edit' ) ) ) );
  276. }
  277. /**
  278. * Get upsell_ids value.
  279. *
  280. * @since 3.1.0
  281. * @param WC_Product $product Product being exported.
  282. * @return string
  283. */
  284. protected function get_column_value_upsell_ids( $product ) {
  285. return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_upsell_ids( 'edit' ) ) ) );
  286. }
  287. /**
  288. * Get parent_id value.
  289. *
  290. * @since 3.1.0
  291. * @param WC_Product $product Product being exported.
  292. * @return string
  293. */
  294. protected function get_column_value_parent_id( $product ) {
  295. if ( $product->get_parent_id( 'edit' ) ) {
  296. $parent = wc_get_product( $product->get_parent_id( 'edit' ) );
  297. if ( ! $parent ) {
  298. return '';
  299. }
  300. return $parent->get_sku( 'edit' ) ? $parent->get_sku( 'edit' ) : 'id:' . $parent->get_id();
  301. }
  302. return '';
  303. }
  304. /**
  305. * Get grouped_products value.
  306. *
  307. * @since 3.1.0
  308. * @param WC_Product $product Product being exported.
  309. * @return string
  310. */
  311. protected function get_column_value_grouped_products( $product ) {
  312. if ( 'grouped' !== $product->get_type() ) {
  313. return '';
  314. }
  315. $grouped_products = array();
  316. $child_ids = $product->get_children( 'edit' );
  317. foreach ( $child_ids as $child_id ) {
  318. $child = wc_get_product( $child_id );
  319. if ( ! $child ) {
  320. continue;
  321. }
  322. $grouped_products[] = $child->get_sku( 'edit' ) ? $child->get_sku( 'edit' ) : 'id:' . $child_id;
  323. }
  324. return $this->implode_values( $grouped_products );
  325. }
  326. /**
  327. * Get download_limit value.
  328. *
  329. * @since 3.1.0
  330. * @param WC_Product $product Product being exported.
  331. * @return string
  332. */
  333. protected function get_column_value_download_limit( $product ) {
  334. return $product->is_downloadable() && $product->get_download_limit( 'edit' ) ? $product->get_download_limit( 'edit' ) : '';
  335. }
  336. /**
  337. * Get download_expiry value.
  338. *
  339. * @since 3.1.0
  340. * @param WC_Product $product Product being exported.
  341. * @return string
  342. */
  343. protected function get_column_value_download_expiry( $product ) {
  344. return $product->is_downloadable() && $product->get_download_expiry( 'edit' ) ? $product->get_download_expiry( 'edit' ) : '';
  345. }
  346. /**
  347. * Get stock value.
  348. *
  349. * @since 3.1.0
  350. * @param WC_Product $product Product being exported.
  351. * @return string
  352. */
  353. protected function get_column_value_stock( $product ) {
  354. $manage_stock = $product->get_manage_stock( 'edit' );
  355. $stock_quantity = $product->get_stock_quantity( 'edit' );
  356. if ( $product->is_type( 'variation' ) && 'parent' === $manage_stock ) {
  357. return 'parent';
  358. } elseif ( $manage_stock ) {
  359. return $stock_quantity;
  360. } else {
  361. return '';
  362. }
  363. }
  364. /**
  365. * Get stock status value.
  366. *
  367. * @since 3.1.0
  368. * @param WC_Product $product Product being exported.
  369. * @return string
  370. */
  371. protected function get_column_value_stock_status( $product ) {
  372. $status = $product->get_stock_status( 'edit' );
  373. if ( 'onbackorder' === $status ) {
  374. return 'backorder';
  375. }
  376. return 'instock' === $status ? 1 : 0;
  377. }
  378. /**
  379. * Get backorders.
  380. *
  381. * @since 3.1.0
  382. * @param WC_Product $product Product being exported.
  383. * @return string
  384. */
  385. protected function get_column_value_backorders( $product ) {
  386. $backorders = $product->get_backorders( 'edit' );
  387. switch ( $backorders ) {
  388. case 'notify':
  389. return 'notify';
  390. default:
  391. return wc_string_to_bool( $backorders ) ? 1 : 0;
  392. }
  393. }
  394. /**
  395. * Get type value.
  396. *
  397. * @since 3.1.0
  398. * @param WC_Product $product Product being exported.
  399. * @return string
  400. */
  401. protected function get_column_value_type( $product ) {
  402. $types = array();
  403. $types[] = $product->get_type();
  404. if ( $product->is_downloadable() ) {
  405. $types[] = 'downloadable';
  406. }
  407. if ( $product->is_virtual() ) {
  408. $types[] = 'virtual';
  409. }
  410. return $this->implode_values( $types );
  411. }
  412. /**
  413. * Export downloads.
  414. *
  415. * @since 3.1.0
  416. * @param WC_Product $product Product being exported.
  417. * @param array $row Row being exported.
  418. */
  419. protected function prepare_downloads_for_export( $product, &$row ) {
  420. if ( $product->is_downloadable() && $this->is_column_exporting( 'downloads' ) ) {
  421. $downloads = $product->get_downloads( 'edit' );
  422. if ( $downloads ) {
  423. $i = 1;
  424. foreach ( $downloads as $download ) {
  425. /* translators: %s: download number */
  426. $this->column_names[ 'downloads:name' . $i ] = sprintf( __( 'Download %d name', 'woocommerce' ), $i );
  427. /* translators: %s: download number */
  428. $this->column_names[ 'downloads:url' . $i ] = sprintf( __( 'Download %d URL', 'woocommerce' ), $i );
  429. $row[ 'downloads:name' . $i ] = $download->get_name();
  430. $row[ 'downloads:url' . $i ] = $download->get_file();
  431. $i++;
  432. }
  433. }
  434. }
  435. }
  436. /**
  437. * Export attributes data.
  438. *
  439. * @since 3.1.0
  440. * @param WC_Product $product Product being exported.
  441. * @param array $row Row being exported.
  442. */
  443. protected function prepare_attributes_for_export( $product, &$row ) {
  444. if ( $this->is_column_exporting( 'attributes' ) ) {
  445. $attributes = $product->get_attributes();
  446. $default_attributes = $product->get_default_attributes();
  447. if ( count( $attributes ) ) {
  448. $i = 1;
  449. foreach ( $attributes as $attribute_name => $attribute ) {
  450. /* translators: %s: attribute number */
  451. $this->column_names[ 'attributes:name' . $i ] = sprintf( __( 'Attribute %d name', 'woocommerce' ), $i );
  452. /* translators: %s: attribute number */
  453. $this->column_names[ 'attributes:value' . $i ] = sprintf( __( 'Attribute %d value(s)', 'woocommerce' ), $i );
  454. /* translators: %s: attribute number */
  455. $this->column_names[ 'attributes:visible' . $i ] = sprintf( __( 'Attribute %d visible', 'woocommerce' ), $i );
  456. /* translators: %s: attribute number */
  457. $this->column_names[ 'attributes:taxonomy' . $i ] = sprintf( __( 'Attribute %d global', 'woocommerce' ), $i );
  458. if ( is_a( $attribute, 'WC_Product_Attribute' ) ) {
  459. $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute->get_name(), $product );
  460. if ( $attribute->is_taxonomy() ) {
  461. $terms = $attribute->get_terms();
  462. $values = array();
  463. foreach ( $terms as $term ) {
  464. $values[] = $term->name;
  465. }
  466. $row[ 'attributes:value' . $i ] = $this->implode_values( $values );
  467. $row[ 'attributes:taxonomy' . $i ] = 1;
  468. } else {
  469. $row[ 'attributes:value' . $i ] = $this->implode_values( $attribute->get_options() );
  470. $row[ 'attributes:taxonomy' . $i ] = 0;
  471. }
  472. $row[ 'attributes:visible' . $i ] = $attribute->get_visible();
  473. } else {
  474. $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute_name, $product );
  475. if ( 0 === strpos( $attribute_name, 'pa_' ) ) {
  476. $option_term = get_term_by( 'slug', $attribute, $attribute_name ); // @codingStandardsIgnoreLine.
  477. $row[ 'attributes:value' . $i ] = $option_term && ! is_wp_error( $option_term ) ? str_replace( ',', '\\,', $option_term->name ) : $attribute;
  478. $row[ 'attributes:taxonomy' . $i ] = 1;
  479. } else {
  480. $row[ 'attributes:value' . $i ] = $attribute;
  481. $row[ 'attributes:taxonomy' . $i ] = 0;
  482. }
  483. $row[ 'attributes:visible' . $i ] = '';
  484. }
  485. if ( $product->is_type( 'variable' ) && isset( $default_attributes[ sanitize_title( $attribute_name ) ] ) ) {
  486. /* translators: %s: attribute number */
  487. $this->column_names[ 'attributes:default' . $i ] = sprintf( __( 'Attribute %d default', 'woocommerce' ), $i );
  488. $default_value = $default_attributes[ sanitize_title( $attribute_name ) ];
  489. if ( 0 === strpos( $attribute_name, 'pa_' ) ) {
  490. $option_term = get_term_by( 'slug', $default_value, $attribute_name ); // @codingStandardsIgnoreLine.
  491. $row[ 'attributes:default' . $i ] = $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $default_value;
  492. } else {
  493. $row[ 'attributes:default' . $i ] = $default_value;
  494. }
  495. }
  496. $i++;
  497. }
  498. }
  499. }
  500. }
  501. /**
  502. * Export meta data.
  503. *
  504. * @since 3.1.0
  505. * @param WC_Product $product Product being exported.
  506. * @param array $row Row data.
  507. */
  508. protected function prepare_meta_for_export( $product, &$row ) {
  509. if ( $this->enable_meta_export ) {
  510. $meta_data = $product->get_meta_data();
  511. if ( count( $meta_data ) ) {
  512. $meta_keys_to_skip = apply_filters( 'woocommerce_product_export_skip_meta_keys', array(), $product );
  513. $i = 1;
  514. foreach ( $meta_data as $meta ) {
  515. if ( in_array( $meta->key, $meta_keys_to_skip, true ) ) {
  516. continue;
  517. }
  518. // Allow 3rd parties to process the meta, e.g. to transform non-scalar values to scalar.
  519. $meta_value = apply_filters( 'woocommerce_product_export_meta_value', $meta->value, $meta, $product, $row );
  520. if ( ! is_scalar( $meta_value ) ) {
  521. continue;
  522. }
  523. $column_key = 'meta:' . esc_attr( $meta->key );
  524. /* translators: %s: meta data name */
  525. $this->column_names[ $column_key ] = sprintf( __( 'Meta: %s', 'woocommerce' ), $meta->key );
  526. $row[ $column_key ] = $meta_value;
  527. $i ++;
  528. }
  529. }
  530. }
  531. }
  532. }