Merge pull request #25064 from woocommerce/fix/24315
Optimize variable product duplication slug generation
This commit is contained in:
commit
6a395e2485
|
@ -1,17 +1,15 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate product functionality
|
||||
*
|
||||
* @author WooCommerce
|
||||
* @category Admin
|
||||
* @package WooCommerce/Admin
|
||||
* @version 3.0.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( class_exists( 'WC_Admin_Duplicate_Product', false ) ) {
|
||||
return new WC_Admin_Duplicate_Product();
|
||||
}
|
||||
|
@ -87,12 +85,10 @@ class WC_Admin_Duplicate_Product {
|
|||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_GET['post'] ) ) {
|
||||
$notify_url = wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . absint( $_GET['post'] ) ), 'woocommerce-duplicate-product_' . $_GET['post'] );
|
||||
?>
|
||||
<div id="duplicate-action"><a class="submitduplicate duplication" href="<?php echo esc_url( $notify_url ); ?>"><?php esc_html_e( 'Copy to a new draft', 'woocommerce' ); ?></a></div>
|
||||
<?php
|
||||
}
|
||||
$notify_url = wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . absint( $post->ID ) ), 'woocommerce-duplicate-product_' . $post->ID );
|
||||
?>
|
||||
<div id="duplicate-action"><a class="submitduplicate duplication" href="<?php echo esc_url( $notify_url ); ?>"><?php esc_html_e( 'Copy to a new draft', 'woocommerce' ); ?></a></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,7 +107,7 @@ class WC_Admin_Duplicate_Product {
|
|||
|
||||
if ( false === $product ) {
|
||||
/* translators: %s: product id */
|
||||
wp_die( sprintf( esc_html__( 'Product creation failed, could not find original product: %s', 'woocommerce' ), $product_id ) );
|
||||
wp_die( sprintf( esc_html__( 'Product creation failed, could not find original product: %s', 'woocommerce' ), esc_html( $product_id ) ) );
|
||||
}
|
||||
|
||||
$duplicate = $this->product_duplicate( $product );
|
||||
|
@ -120,7 +116,7 @@ class WC_Admin_Duplicate_Product {
|
|||
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 ) ), '3.0', 'Use woocommerce_product_duplicate action instead.' );
|
||||
|
||||
// Redirect to the edit screen for the new draft page
|
||||
// Redirect to the edit screen for the new draft page.
|
||||
wp_redirect( admin_url( 'post.php?action=edit&post=' . $duplicate->get_id() ) );
|
||||
exit;
|
||||
}
|
||||
|
@ -128,15 +124,20 @@ class WC_Admin_Duplicate_Product {
|
|||
/**
|
||||
* Function to create the duplicate of the product.
|
||||
*
|
||||
* @param WC_Product $product
|
||||
* @return WC_Product
|
||||
* @param WC_Product $product The product to duplicate.
|
||||
* @return WC_Product The duplicate.
|
||||
*/
|
||||
public function product_duplicate( $product ) {
|
||||
// Filter to allow us to unset/remove data we don't want to copy to the duplicate. @since 2.6
|
||||
/**
|
||||
* 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 );
|
||||
/* translators: %s contains the name of the original product. */
|
||||
$duplicate->set_name( sprintf( esc_html__( '%s (Copy)', 'woocommerce' ), $duplicate->get_name() ) );
|
||||
$duplicate->set_total_sales( 0 );
|
||||
if ( '' !== $product->get_sku( 'edit' ) ) {
|
||||
|
@ -153,7 +154,11 @@ class WC_Admin_Duplicate_Product {
|
|||
$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 3.0
|
||||
/**
|
||||
* This action can be used to modify the object further before it is created - it will be passed by reference.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
do_action( 'woocommerce_product_duplicate_before_save', $duplicate, $product );
|
||||
|
||||
// Save parent product.
|
||||
|
@ -168,6 +173,12 @@ class WC_Admin_Duplicate_Product {
|
|||
$child_duplicate->set_id( 0 );
|
||||
$child_duplicate->set_date_created( null );
|
||||
|
||||
// If we wait and let the insertion generate the slug, we will see extreme performance degradation
|
||||
// in the case where a product is used as a template. Every time the template is duplicated, each
|
||||
// variation will query every consecutive slug until it finds an empty one. To avoid this, we can
|
||||
// optimize the generation ourselves, avoiding the issue altogether.
|
||||
$this->generate_unique_slug( $child_duplicate );
|
||||
|
||||
if ( '' !== $child->get_sku( 'edit' ) ) {
|
||||
$child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku( 'edit' ) ) );
|
||||
}
|
||||
|
@ -176,7 +187,11 @@ class WC_Admin_Duplicate_Product {
|
|||
$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 3.0
|
||||
/**
|
||||
* This action can be used to modify the object further before it is created - it will be passed by reference.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
do_action( 'woocommerce_product_duplicate_before_save', $child_duplicate, $child );
|
||||
|
||||
$child_duplicate->save();
|
||||
|
@ -193,7 +208,7 @@ class WC_Admin_Duplicate_Product {
|
|||
* Get a product from the database to duplicate.
|
||||
*
|
||||
* @deprecated 3.0.0
|
||||
* @param mixed $id
|
||||
* @param mixed $id The ID of the product to duplicate.
|
||||
* @return object|bool
|
||||
* @see duplicate_product
|
||||
*/
|
||||
|
@ -215,6 +230,44 @@ class WC_Admin_Duplicate_Product {
|
|||
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique slug for a given product. We do this so that we can override the
|
||||
* behavior of wp_unique_post_slug(). The normal slug generation will run single
|
||||
* select queries on every non-unique slug, resulting in very bad performance.
|
||||
*
|
||||
* @param WC_Product $product The product to generate a slug for.
|
||||
* @since 3.9.0
|
||||
*/
|
||||
private function generate_unique_slug( $product ) {
|
||||
global $wpdb;
|
||||
|
||||
// We want to remove the suffix from the slug so that we can find the maximum suffix using this root slug.
|
||||
// This will allow us to find the next-highest suffix that is unique. While this does not support gap
|
||||
// filling, this shouldn't matter for our use-case.
|
||||
$root_slug = preg_replace( '/-[0-9]+$/', '', $product->get_slug() );
|
||||
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare( "SELECT post_name FROM $wpdb->posts WHERE post_name LIKE %s AND post_type IN ( 'product', 'product_variation' )", $root_slug . '%' )
|
||||
);
|
||||
|
||||
// The slug is already unique!
|
||||
if ( empty( $results ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the maximum suffix so we can ensure uniqueness.
|
||||
$max_suffix = 1;
|
||||
foreach ( $results as $result ) {
|
||||
// Pull a numerical suffix off the slug after the last hyphen.
|
||||
$suffix = intval( substr( $result->post_name, strrpos( $result->post_name, '-' ) + 1 ) );
|
||||
if ( $suffix > $max_suffix ) {
|
||||
$max_suffix = $suffix;
|
||||
}
|
||||
}
|
||||
|
||||
$product->set_slug( $root_slug . '-' . ( $max_suffix + 1 ) );
|
||||
}
|
||||
}
|
||||
|
||||
return new WC_Admin_Duplicate_Product();
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
/**
|
||||
* Unit tests for the admin product duplication class.
|
||||
*
|
||||
* @package WooCommerce\Tests\Admin
|
||||
*/
|
||||
|
||||
/**
|
||||
* WC_Tests_Admin_Duplicate_Product tests.
|
||||
*
|
||||
* @package WooCommerce\Tests\Admin
|
||||
* @since 3.9.0
|
||||
*/
|
||||
class WC_Tests_Admin_Duplicate_Product extends WC_Unit_Test_Case {
|
||||
/**
|
||||
* Test duplication of a simple product.
|
||||
*/
|
||||
public function test_simple_product_duplication() {
|
||||
$product = WC_Helper_Product::create_simple_product();
|
||||
|
||||
$duplicate = ( new WC_Admin_Duplicate_Product() )->product_duplicate( $product );
|
||||
|
||||
$this->assertNotEquals( $product->get_id(), $duplicate->get_id() );
|
||||
$this->assertEquals( $product->get_name() . ' (Copy)', $duplicate->get_name() );
|
||||
$this->assertEquals( 'draft', $duplicate->get_status() );
|
||||
$this->assertDuplicateWasReset( $duplicate );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test duplication of a variable product.
|
||||
*/
|
||||
public function test_variable_product_duplication() {
|
||||
$product = WC_Helper_Product::create_variation_product();
|
||||
|
||||
$duplicate = ( new WC_Admin_Duplicate_Product() )->product_duplicate( $product );
|
||||
|
||||
$this->assertNotEquals( $product->get_id(), $duplicate->get_id() );
|
||||
$this->assertEquals( $product->get_name() . ' (Copy)', $duplicate->get_name() );
|
||||
$this->assertEquals( 'draft', $duplicate->get_status() );
|
||||
$this->assertDuplicateWasReset( $duplicate );
|
||||
|
||||
$product_children = $product->get_children();
|
||||
$duplicate_children = $duplicate->get_children();
|
||||
$child_count = count( $product_children );
|
||||
$this->assertEquals( $child_count, count( $duplicate_children ) );
|
||||
|
||||
for ( $i = 0; $i < $child_count; $i++ ) {
|
||||
$product_child = wc_get_product( $product_children[ $i ] );
|
||||
$duplicate_child = wc_get_product( $duplicate_children[ $i ] );
|
||||
|
||||
$this->assertNotEquals( $product_child->get_id(), $duplicate_child->get_id() );
|
||||
$this->assertEquals( $product_child->get_name() . ' (Copy)', $duplicate_child->get_name() );
|
||||
$this->assertEquals( 'publish', $duplicate_child->get_status() );
|
||||
$this->assertDuplicateWasReset( $duplicate_child );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the unique slugs generated by variation duplication are correct.
|
||||
*/
|
||||
public function test_variable_product_duplication_unique_slug_generation() {
|
||||
$product = WC_Helper_Product::create_variation_product();
|
||||
|
||||
// We just want to make sure each successive duplication has the correct slugs.
|
||||
$slug_matches = array(
|
||||
array(
|
||||
'dummy-variable-product-small-2',
|
||||
'dummy-variable-product-large-2',
|
||||
'dummy-variable-product-3',
|
||||
'dummy-variable-product-4',
|
||||
),
|
||||
array(
|
||||
'dummy-variable-product-small-3',
|
||||
'dummy-variable-product-large-3',
|
||||
'dummy-variable-product-5',
|
||||
'dummy-variable-product-6',
|
||||
),
|
||||
array(
|
||||
'dummy-variable-product-small-4',
|
||||
'dummy-variable-product-large-4',
|
||||
'dummy-variable-product-7',
|
||||
'dummy-variable-product-8',
|
||||
),
|
||||
);
|
||||
|
||||
foreach ( $slug_matches as $slug_match ) {
|
||||
$duplicate = ( new WC_Admin_Duplicate_Product() )->product_duplicate( $product );
|
||||
|
||||
$duplicate_children = $duplicate->get_children();
|
||||
|
||||
$this->assertEquals( 4, count( $duplicate_children ) );
|
||||
foreach ( $slug_match as $key => $slug ) {
|
||||
$child = wc_get_product( $duplicate_children[ $key ] );
|
||||
$this->assertEquals( $slug, $child->get_slug() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the product was correctly reset after duplication.
|
||||
*
|
||||
* @param WC_Product $duplicate The duplicate product to evaluate.
|
||||
*/
|
||||
private function assertDuplicateWasReset( $duplicate ) {
|
||||
$this->assertEquals( 0, $duplicate->get_total_sales() );
|
||||
$this->assertEquals( array(), $duplicate->get_rating_counts() );
|
||||
$this->assertEquals( 0, $duplicate->get_average_rating() );
|
||||
$this->assertEquals( 0, $duplicate->get_rating_count() );
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue