diff --git a/includes/admin/class-wc-admin-duplicate-product.php b/includes/admin/class-wc-admin-duplicate-product.php index d4240f7ea7e..e2cb996b1d3 100644 --- a/includes/admin/class-wc-admin-duplicate-product.php +++ b/includes/admin/class-wc-admin-duplicate-product.php @@ -94,32 +94,50 @@ class WC_Admin_Duplicate_Product { wp_die( sprintf( __( 'Product creation failed, could not find original product: %s', 'woocommerce' ), $product_id ) ); } + // Filter to allow us to unset/remove data we don't want to copy to the duplicate. @since 2.6 + $meta_to_exclude = array_filter( apply_filters( 'woocommerce_duplicate_product_exclude_meta', array() ) ); + $duplicate = clone $product; $duplicate->set_id( 0 ); - $duplicate->save(); + $duplicate->set_total_sales( 0 ); + if ( '' !== $product->get_sku() ) { + $duplicate->set_sku( wc_product_generate_unique_sku( 0, $product->get_sku() ) ); + } + $duplicate->set_status( 'draft' ); - $sku = $duplicate->get_sku(); - if ( '' !== $duplicate->get_sku() ) { - wc_product_force_unique_sku( $duplicate->get_id() ); + foreach ( $meta_to_exclude as $meta_key ) { + $duplicate->delete_meta_data( $meta_key ); } - $exclude = apply_filters( 'woocommerce_duplicate_product_exclude_children', false ); - - if ( ! $exclude && ( $product->is_type( 'variable' ) || $product->is_type( 'grouped' ) ) ) { - foreach( $product->get_children() as $child_id ) { - $child = wc_get_product( $child_id ); + // This action can be used to modify the object further before it is created - it will be passed by reference. @since 2.7 + do_action( 'woocommerce_product_duplicate_before_save', $duplicate, $product ); + + // Save parent product. + $duplicate->save(); + + if ( ! apply_filters( 'woocommerce_duplicate_product_exclude_children', false ) && ( $product->is_type( 'variable' ) || $product->is_type( 'grouped' ) ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); $child_duplicate = clone $child; $child_duplicate->set_parent_id( $duplicate->get_id() ); $child_duplicate->set_id( 0 ); - $child_duplicate->save(); - if ( '' !== $child_duplicate->get_sku() ) { - wc_product_force_unique_sku( $child_duplicate->get_id() ); + + if ( '' !== $child->get_sku() ) { + $child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku() ) ); } + + foreach ( $meta_to_exclude as $meta_key ) { + $child_duplicate->delete_meta_data( $meta_key ); + } + + // This action can be used to modify the object further before it is created - it will be passed by reference. @since 2.7 + do_action( 'woocommerce_product_duplicate_before_save', $child_duplicate, $child ); + + $child_duplicate->save(); } } // Hook rename to match other woocommerce_product_* hooks, and to move away from depending on a response from the wp_posts table. - // New hook returns new id and old id. do_action( 'woocommerce_product_duplicate', $duplicate, $product ); wc_do_deprecated_action( 'woocommerce_duplicate_product', array( $duplicate->get_id(), $this->get_product_to_duplicate( $product_id ) ), '2.7', 'Use woocommerce_product_duplicate action instead.' ); diff --git a/includes/data-stores/class-wc-product-data-store-cpt.php b/includes/data-stores/class-wc-product-data-store-cpt.php index 592276d930b..7df9a2caa5b 100644 --- a/includes/data-stores/class-wc-product-data-store-cpt.php +++ b/includes/data-stores/class-wc-product-data-store-cpt.php @@ -98,14 +98,12 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da if ( $id && ! is_wp_error( $id ) ) { $product->set_id( $id ); - $this->updated_props = array(); - $this->update_post_meta( $product ); - $this->handle_updated_props( $product ); - - $this->update_terms( $product ); - $this->update_visibility( $product ); - $this->update_attributes( $product ); + $this->update_post_meta( $product, true ); + $this->update_terms( $product, true ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product, true ); $this->update_version_and_type( $product ); + $this->handle_updated_props( $product ); $product->save_meta_data(); $product->apply_changes(); @@ -162,24 +160,22 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order' ), array_keys( $changes ) ) ) { wp_update_post( array( 'ID' => $product->get_id(), - 'post_content' => $product->get_description(), - 'post_excerpt' => $product->get_short_description(), - 'post_title' => $product->get_name(), - 'post_parent' => $product->get_parent_id(), - 'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed', - 'post_status' => $product->get_status() ? $product->get_status() : 'publish', - 'menu_order' => $product->get_menu_order(), + 'post_content' => $product->get_description( 'edit' ), + 'post_excerpt' => $product->get_short_description( 'edit' ), + 'post_title' => $product->get_name( 'edit' ), + 'post_parent' => $product->get_parent_id( 'edit' ), + 'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed', + 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', + 'menu_order' => $product->get_menu_order( 'edit' ), ) ); } - $this->updated_props = array(); $this->update_post_meta( $product ); - $this->handle_updated_props( $product ); - $this->update_terms( $product ); $this->update_visibility( $product ); $this->update_attributes( $product ); $this->update_version_and_type( $product ); + $this->handle_updated_props( $product ); $product->save_meta_data(); $product->apply_changes(); @@ -385,10 +381,10 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. * * @param WC_Product + * @param bool Force update. Used during create. * @since 2.7.0 */ - protected function update_post_meta( &$product ) { - + protected function update_post_meta( &$product, $force = false ) { $meta_key_to_props = array( '_sku' => 'sku', '_regular_price' => 'regular_price', @@ -424,11 +420,12 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da // Make sure to take extra data (like product url or text for external products) into account. $extra_data_keys = $product->get_extra_data_keys(); + foreach ( $extra_data_keys as $key ) { $meta_key_to_props[ '_' . $key ] = $key; } - $props_to_update = $this->get_props_to_update( $product, $meta_key_to_props ); + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); foreach ( $props_to_update as $meta_key => $prop ) { $value = $product->{"get_$prop"}( 'edit' ); @@ -474,19 +471,18 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da } } - if ( $this->update_downloads( $product ) ) { + if ( $this->update_downloads( $product, $force ) ) { $this->updated_props[] = 'downloads'; } } /** - * Handle updated meta props after updating meta. + * Handle updated meta props after updating meta data. * * @since 2.7.0 * @param WC_Product $product */ protected function handle_updated_props( &$product ) { - if ( in_array( 'date_on_sale_from', $this->updated_props ) || in_array( 'date_on_sale_to', $this->updated_props ) || in_array( 'regular_price', $this->updated_props ) || in_array( 'sale_price', $this->updated_props ) ) { if ( $product->is_on_sale( 'edit' ) ) { update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) ); @@ -505,25 +501,30 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da do_action( $product->is_type( 'variation' ) ? 'woocommerce_variation_set_stock_status' : 'woocommerce_product_set_stock_status' , $product->get_id(), $product->get_stock_status(), $product ); } + // Trigger action so 3rd parties can deal with updated props. do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props ); + + // After handling, we can reset the props array. + $this->updated_props = array(); } /** * For all stored terms in all taxonomies, save them to the DB. * * @param WC_Product + * @param bool Force update. Used during create. * @since 2.7.0 */ - protected function update_terms( &$product ) { + protected function update_terms( &$product, $force = false ) { $changes = $product->get_changes(); - if ( array_key_exists( 'category_ids', $changes ) ) { + if ( $force || array_key_exists( 'category_ids', $changes ) ) { wp_set_post_terms( $product->get_id(), $product->get_category_ids( 'edit' ), 'product_cat', false ); } - if ( array_key_exists( 'tag_ids', $changes ) ) { + if ( $force || array_key_exists( 'tag_ids', $changes ) ) { wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false ); } - if ( array_key_exists( 'shipping_class_id', $changes ) ) { + if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); } } @@ -532,12 +533,13 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da * Update visibility terms based on props. * * @since 2.7.0 + * @param bool Force update. Used during create. * @param WC_Product */ - protected function update_visibility( &$product ) { + protected function update_visibility( &$product, $force = false ) { $changes = $product->get_changes(); - if ( array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) { + if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) { $terms = array(); if ( $product->get_featured() ) { @@ -575,12 +577,13 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da * Update attributes which are a mix of terms and meta data. * * @param WC_Product + * @param bool Force update. Used during create. * @since 2.7.0 */ - protected function update_attributes( &$product ) { + protected function update_attributes( &$product, $force = false ) { $changes = $product->get_changes(); - if ( array_key_exists( 'attributes', $changes ) ) { + if ( $force || array_key_exists( 'attributes', $changes ) ) { $attributes = $product->get_attributes(); $meta_values = array(); @@ -622,12 +625,13 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da * * @since 2.7.0 * @param WC_Product $product + * @param bool Force update. Used during create. * @return bool If updated or not. */ - protected function update_downloads( &$product ) { + protected function update_downloads( &$product, $force = false ) { $changes = $product->get_changes(); - if ( array_key_exists( 'downloads', $changes ) ) { + if ( $force || array_key_exists( 'downloads', $changes ) ) { $downloads = $product->get_downloads(); $meta_values = array(); diff --git a/includes/data-stores/class-wc-product-grouped-data-store-cpt.php b/includes/data-stores/class-wc-product-grouped-data-store-cpt.php index 3b11c3c6952..4ce09899b26 100644 --- a/includes/data-stores/class-wc-product-grouped-data-store-cpt.php +++ b/includes/data-stores/class-wc-product-grouped-data-store-cpt.php @@ -16,10 +16,35 @@ class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implem * Helper method that updates all the post meta for a grouped product. * * @param WC_Product + * @param bool Force update. Used during create. * @since 2.7.0 */ - protected function update_post_meta( &$product ) { - if ( update_post_meta( $product->get_id(), '_children', $product->get_children( 'edit' ) ) ) { + protected function update_post_meta( &$product, $force = false ) { + $meta_key_to_props = array( + '_children' => 'children', + ); + + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + $updated = update_post_meta( $product->get_id(), $meta_key, $value ); + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + parent::update_post_meta( $product ); + } + + /** + * Handle updated meta props after updating meta data. + * + * @since 2.7.0 + * @param WC_Product $product + */ + protected function handle_updated_props( &$product ) { + if ( in_array( 'children', $this->updated_props ) ) { $child_prices = array(); foreach ( $product->get_children( 'edit' ) as $child_id ) { $child = wc_get_product( $child_id ); @@ -34,11 +59,8 @@ class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implem add_post_meta( $product->get_id(), '_price', min( $child_prices ) ); add_post_meta( $product->get_id(), '_price', max( $child_prices ) ); } - - $this->extra_data_saved = true; } - - parent::update_post_meta( $product ); + parent::handle_updated_props( $product ); } /** diff --git a/includes/data-stores/class-wc-product-variation-data-store-cpt.php b/includes/data-stores/class-wc-product-variation-data-store-cpt.php index 734f3f88813..9a322b12b80 100644 --- a/includes/data-stores/class-wc-product-variation-data-store-cpt.php +++ b/includes/data-stores/class-wc-product-variation-data-store-cpt.php @@ -113,13 +113,11 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl if ( $id && ! is_wp_error( $id ) ) { $product->set_id( $id ); - $this->updated_props = array(); - $this->update_post_meta( $product ); + $this->update_post_meta( $product, true ); + $this->update_terms( $product, true ); + $this->update_attributes( $product, true ); $this->handle_updated_props( $product ); - $this->update_terms( $product ); - $this->update_attributes( $product ); - $product->save_meta_data(); $product->apply_changes(); @@ -138,23 +136,25 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl * @param WC_Product */ public function update( &$product ) { - $post_data = array( - 'ID' => $product->get_id(), - 'post_title' => $this->generate_product_title( $product ), - 'post_parent' => $product->get_parent_id(), - 'comment_status' => 'closed', - 'post_status' => $product->get_status() ? $product->get_status() : 'publish', - 'menu_order' => $product->get_menu_order(), - ); + $changes = $product->get_changes(); + $title = $this->generate_product_title( $product ); - wp_update_post( $post_data ); + // Only update the post when the post data changes. + if ( $title !== $product->get_name( 'edit' ) || array_intersect( array( 'parent_id', 'status', 'menu_order' ), array_keys( $changes ) ) ) { + wp_update_post( array( + 'ID' => $product->get_id(), + 'post_title' => $title, + 'post_parent' => $product->get_parent_id( 'edit' ), + 'comment_status' => 'closed', + 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', + 'menu_order' => $product->get_menu_order( 'edit' ), + ) ); + } - $this->updated_props = array(); $this->update_post_meta( $product ); - $this->handle_updated_props( $product ); - $this->update_terms( $product ); $this->update_attributes( $product ); + $this->handle_updated_props( $product ); $product->save_meta_data(); $product->apply_changes(); @@ -283,9 +283,14 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl * * @since 2.7.0 * @param WC_Product + * @param bool Force update. Used during create. */ - protected function update_terms( &$product ) { - wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); + protected function update_terms( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { + wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); + } } /** @@ -293,21 +298,26 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl * * @since 2.7.0 * @param WC_Product + * @param bool Force update. Used during create. */ - protected function update_attributes( &$product ) { - global $wpdb; - $attributes = $product->get_attributes(); - $updated_attribute_keys = array(); - foreach ( $attributes as $key => $value ) { - update_post_meta( $product->get_id(), 'attribute_' . $key, $value ); - $updated_attribute_keys[] = 'attribute_' . $key; - } + protected function update_attributes( &$product, $force = false ) { + $changes = $product->get_changes(); - // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. - $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d;", $product->get_id() ) ); + if ( $force || array_key_exists( 'attributes', $changes ) ) { + global $wpdb; + $attributes = $product->get_attributes(); + $updated_attribute_keys = array(); + foreach ( $attributes as $key => $value ) { + update_post_meta( $product->get_id(), 'attribute_' . $key, $value ); + $updated_attribute_keys[] = 'attribute_' . $key; + } - foreach ( $delete_attribute_keys as $key ) { - delete_post_meta( $product->get_id(), $key ); + // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. + $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d;", $product->get_id() ) ); + + foreach ( $delete_attribute_keys as $key ) { + delete_post_meta( $product->get_id(), $key ); + } } } @@ -316,12 +326,21 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl * * @since 2.7.0 * @param WC_Product + * @param bool Force update. Used during create. */ - public function update_post_meta( &$product ) { - $updated_description = update_post_meta( $product->get_id(), '_variation_description', $product->get_description() ); + public function update_post_meta( &$product, $force = false ) { + $meta_key_to_props = array( + '_variation_description' => 'description', + ); - if ( $updated_description ) { - $this->updated_props[] = 'description'; + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + $updated = update_post_meta( $product->get_id(), $meta_key, $value ); + if ( $updated ) { + $this->updated_props[] = $prop; + } } parent::update_post_meta( $product );