Merge pull request #29778 from woocommerce/add/29608
Product attributes lookup table creation and filling
This commit is contained in:
commit
f9441dcc00
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
|
@ -37,7 +38,8 @@ class WC_Admin_Status {
|
|||
wp_die( 'Cannot load the REST API to access WC_REST_System_Status_Tools_Controller.' );
|
||||
}
|
||||
|
||||
$tools = self::get_tools();
|
||||
$tools = self::get_tools();
|
||||
$tool_requires_refresh = false;
|
||||
|
||||
if ( ! empty( $_GET['action'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'debug_action' ) ) { // WPCS: input var ok, sanitization ok.
|
||||
$tools_controller = new WC_REST_System_Status_Tools_Controller();
|
||||
|
@ -46,14 +48,16 @@ class WC_Admin_Status {
|
|||
if ( array_key_exists( $action, $tools ) ) {
|
||||
$response = $tools_controller->execute_tool( $action );
|
||||
|
||||
$tool = $tools[ $action ];
|
||||
$tool = array(
|
||||
$tool = $tools[ $action ];
|
||||
$tool_requires_refresh = ArrayUtil::get_value_or_default( $tool, 'requires_refresh', false );
|
||||
$tool = array(
|
||||
'id' => $action,
|
||||
'name' => $tool['name'],
|
||||
'action' => $tool['button'],
|
||||
'description' => $tool['desc'],
|
||||
'disabled' => ArrayUtil::get_value_or_default( $tool, 'disabled', false ),
|
||||
);
|
||||
$tool = array_merge( $tool, $response );
|
||||
$tool = array_merge( $tool, $response );
|
||||
|
||||
/**
|
||||
* Fires after a WooCommerce system status tool has been executed.
|
||||
|
@ -80,6 +84,10 @@ class WC_Admin_Status {
|
|||
echo '<div class="updated inline"><p>' . esc_html__( 'Your changes have been saved.', 'woocommerce' ) . '</p></div>';
|
||||
}
|
||||
|
||||
if ( $tool_requires_refresh ) {
|
||||
$tools = self::get_tools();
|
||||
}
|
||||
|
||||
include_once __DIR__ . '/views/html-admin-page-status-tools.php';
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin View: Page - Status Tools
|
||||
*
|
||||
* @package WooCommerce
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
@ -12,14 +16,14 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
<?php settings_fields( 'woocommerce_status_settings_fields' ); ?>
|
||||
<table class="wc_status_table wc_status_table--tools widefat" cellspacing="0">
|
||||
<tbody class="tools">
|
||||
<?php foreach ( $tools as $action => $tool ) : ?>
|
||||
<tr class="<?php echo sanitize_html_class( $action ); ?>">
|
||||
<?php foreach ( $tools as $action_name => $tool ) : ?>
|
||||
<tr class="<?php echo sanitize_html_class( $action_name ); ?>">
|
||||
<th>
|
||||
<strong class="name"><?php echo esc_html( $tool['name'] ); ?></strong>
|
||||
<p class="description"><?php echo wp_kses_post( $tool['desc'] ); ?></p>
|
||||
</th>
|
||||
<td class="run-tool">
|
||||
<a href="<?php echo wp_nonce_url( admin_url( 'admin.php?page=wc-status&tab=tools&action=' . $action ), 'debug_action' ); ?>" class="button button-large <?php echo esc_attr( $action ); ?>"><?php echo esc_html( $tool['button'] ); ?></a>
|
||||
<a <?php echo ArrayUtil::is_truthy( $tool, 'disabled' ) ? 'disabled' : ''; ?> href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=wc-status&tab=tools&action=' . $action_name ), 'debug_action' ) ); ?>" class="button button-large <?php echo esc_attr( $action_name ); ?>"><?php echo esc_html( $tool['button'] ); ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
|
|
@ -10,6 +10,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
|
||||
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
|
||||
use Automattic\WooCommerce\Internal\AssignDefaultCategory;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
|
||||
/**
|
||||
|
@ -209,6 +210,7 @@ final class WooCommerce {
|
|||
// These classes set up hooks on instantiation.
|
||||
wc_get_container()->get( DownloadPermissionsAdjuster::class );
|
||||
wc_get_container()->get( AssignDefaultCategory::class );
|
||||
wc_get_container()->get( DataRegenerator::class );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -143,7 +143,7 @@ class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller {
|
|||
'button' => __( 'Clean up download permissions', 'woocommerce' ),
|
||||
'desc' => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ),
|
||||
),
|
||||
'regenerate_product_lookup_tables' => array(
|
||||
'regenerate_product_lookup_tables' => array(
|
||||
'name' => __( 'Product lookup tables', 'woocommerce' ),
|
||||
'button' => __( 'Regenerate', 'woocommerce' ),
|
||||
'desc' => __( 'This tool will regenerate product lookup table data. This process may take a while.', 'woocommerce' ),
|
||||
|
@ -552,14 +552,14 @@ class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller {
|
|||
$message = __( 'Template cache cleared.', 'woocommerce' );
|
||||
} else {
|
||||
$message = __( 'The active version of WooCommerce does not support template cache clearing.', 'woocommerce' );
|
||||
$ran = false;
|
||||
$ran = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'verify_db_tables':
|
||||
if ( ! method_exists( 'WC_Install', 'verify_base_tables' ) ) {
|
||||
$message = __( 'You need WooCommerce 4.2 or newer to run this tool.', 'woocommerce' );
|
||||
$ran = false;
|
||||
$ran = false;
|
||||
break;
|
||||
}
|
||||
// Try to manually create table again.
|
||||
|
@ -567,9 +567,9 @@ class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller {
|
|||
if ( 0 === count( $missing_tables ) ) {
|
||||
$message = __( 'Database verified successfully.', 'woocommerce' );
|
||||
} else {
|
||||
$message = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' );
|
||||
$message = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' );
|
||||
$message .= implode( ', ', $missing_tables );
|
||||
$ran = false;
|
||||
$ran = false;
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -577,11 +577,35 @@ class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller {
|
|||
$tools = $this->get_tools();
|
||||
if ( isset( $tools[ $tool ]['callback'] ) ) {
|
||||
$callback = $tools[ $tool ]['callback'];
|
||||
$return = call_user_func( $callback );
|
||||
if ( is_string( $return ) ) {
|
||||
try {
|
||||
$return = call_user_func( $callback );
|
||||
} catch ( Exception $exception ) {
|
||||
$return = $exception;
|
||||
}
|
||||
if ( is_a( $return, Exception::class ) ) {
|
||||
$callback_string = $this->get_printable_callback_name( $callback, $tool );
|
||||
$ran = false;
|
||||
/* translators: %1$s: callback string, %2$s: error message */
|
||||
$message = sprintf( __( 'There was an error calling %1$s: %2$s', 'woocommerce' ), $callback_string, $return->getMessage() );
|
||||
|
||||
$logger = wc_get_logger();
|
||||
$logger->error(
|
||||
sprintf(
|
||||
'Error running debug tool %s: %s',
|
||||
$tool,
|
||||
$return->getMessage()
|
||||
),
|
||||
array(
|
||||
'source' => 'run-debug-tool',
|
||||
'tool' => $tool,
|
||||
'callback' => $callback,
|
||||
'error' => $return,
|
||||
)
|
||||
);
|
||||
} elseif ( is_string( $return ) ) {
|
||||
$message = $return;
|
||||
} elseif ( false === $return ) {
|
||||
$callback_string = is_array( $callback ) ? get_class( $callback[0] ) . '::' . $callback[1] : $callback;
|
||||
$callback_string = $this->get_printable_callback_name( $callback, $tool );
|
||||
$ran = false;
|
||||
/* translators: %s: callback string */
|
||||
$message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string );
|
||||
|
@ -600,4 +624,22 @@ class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller {
|
|||
'message' => $message,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a printable name for a callback.
|
||||
*
|
||||
* @param mixed $callback The callback to get a name for.
|
||||
* @param string $default The default name, to be returned when the callback is an inline function.
|
||||
* @return string A printable name for the callback.
|
||||
*/
|
||||
private function get_printable_callback_name( $callback, $default ) {
|
||||
if ( is_array( $callback ) ) {
|
||||
return get_class( $callback[0] ) . '::' . $callback[1];
|
||||
}
|
||||
if ( is_string( $callback ) ) {
|
||||
return $callback;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace Automattic\WooCommerce;
|
|||
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProductAttributesLookupServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ThemeManagementServiceProvider;
|
||||
|
||||
|
@ -35,10 +36,11 @@ final class Container implements \Psr\Container\ContainerInterface {
|
|||
* @var string[]
|
||||
*/
|
||||
private $service_providers = array(
|
||||
AssignDefaultCategoryServiceProvider::class,
|
||||
DownloadPermissionsAdjusterServiceProvider::class,
|
||||
ProductAttributesLookupServiceProvider::class,
|
||||
ProxiesServiceProvider::class,
|
||||
ThemeManagementServiceProvider::class,
|
||||
DownloadPermissionsAdjusterServiceProvider::class,
|
||||
AssignDefaultCategoryServiceProvider::class,
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -81,7 +81,7 @@ class ExtendedContainer extends BaseContainer {
|
|||
}
|
||||
|
||||
$concrete_class = $this->get_class_from_concrete( $concrete );
|
||||
if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) ) {
|
||||
if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) && ! $this->is_anonymous_class( $concrete_class ) ) {
|
||||
throw new ContainerException( "You cannot use concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
|
||||
}
|
||||
|
||||
|
@ -149,4 +149,14 @@ class ExtendedContainer extends BaseContainer {
|
|||
protected function is_class_allowed( string $class_name ): bool {
|
||||
return StringUtil::starts_with( $class_name, $this->woocommerce_namespace, false ) || in_array( $class_name, $this->registration_whitelist, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class name corresponds to an anonymous class.
|
||||
*
|
||||
* @param string $class_name The class name to check.
|
||||
* @return bool True if the name corresponds to an anonymous class.
|
||||
*/
|
||||
protected function is_anonymous_class( string $class_name ): bool {
|
||||
return StringUtil::starts_with( $class_name, 'class@anonymous' );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
/**
|
||||
* ProductAttributesLookupServiceProvider class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
|
||||
/**
|
||||
* Service provider for the ProductAttributesLookupServiceProvider namespace.
|
||||
*/
|
||||
class ProductAttributesLookupServiceProvider extends AbstractServiceProvider {
|
||||
|
||||
/**
|
||||
* The classes/interfaces that are serviced by this service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $provides = array(
|
||||
DataRegenerator::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share( DataRegenerator::class )->addArgument( LookupDataStore::class );
|
||||
$this->share( LookupDataStore::class );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,396 @@
|
|||
<?php
|
||||
/**
|
||||
* DataRegenerator class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* This class handles the (re)generation of the product attributes lookup table.
|
||||
* It schedules the regeneration in small product batches by itself, so it can be used outside the
|
||||
* regular WooCommerce data regenerations mechanism.
|
||||
*
|
||||
* After the regeneration is completed a wp_wc_product_attributes_lookup table will exist with entries for
|
||||
* all the products that existed when initiate_regeneration was invoked; entries for products created after that
|
||||
* are supposed to be created/updated by the appropriate data store classes (or by the code that uses
|
||||
* the data store classes) whenever a product is created/updated.
|
||||
*
|
||||
* Additionally, after the regeneration is completed a 'woocommerce_attribute_lookup__enabled' option
|
||||
* with a value of 'no' will have been created.
|
||||
*
|
||||
* This class also adds two entries to the Status - Tools menu: one for manually regenerating the table contents,
|
||||
* and another one for enabling or disabling the actual lookup table usage.
|
||||
*/
|
||||
class DataRegenerator {
|
||||
|
||||
const PRODUCTS_PER_GENERATION_STEP = 10;
|
||||
|
||||
/**
|
||||
* The data store to use.
|
||||
*
|
||||
* @var LookupDataStore
|
||||
*/
|
||||
private $data_store;
|
||||
|
||||
/**
|
||||
* The lookup table name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $lookup_table_name;
|
||||
|
||||
/**
|
||||
* DataRegenerator constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
|
||||
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
|
||||
|
||||
add_filter(
|
||||
'woocommerce_debug_tools',
|
||||
function( $tools ) {
|
||||
return $this->add_initiate_regeneration_entry_to_tools_array( $tools );
|
||||
},
|
||||
1,
|
||||
999
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_run_product_attribute_lookup_update_callback',
|
||||
function () {
|
||||
$this->run_regeneration_step_callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class initialization, invoked by the DI container.
|
||||
*
|
||||
* @internal
|
||||
* @param LookupDataStore $data_store The data store to use.
|
||||
*/
|
||||
final public function init( LookupDataStore $data_store ) {
|
||||
$this->data_store = $data_store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the regeneration procedure:
|
||||
* deletes the lookup table and related options if they exist,
|
||||
* then it creates the table and runs the first step of the regeneration process.
|
||||
*
|
||||
* This is the method that should be used as a callback for a data regeneration in wc-update-functions, e.g.:
|
||||
*
|
||||
* function wc_update_XX_regenerate_product_attributes_lookup_table() {
|
||||
* wc_get_container()->get(DataRegenerator::class)->initiate_regeneration();
|
||||
* return false;
|
||||
* }
|
||||
*
|
||||
* (Note how we are returning "false" since the class handles the step scheduling by itself).
|
||||
*/
|
||||
public function initiate_regeneration() {
|
||||
$this->delete_all_attributes_lookup_data();
|
||||
$products_exist = $this->initialize_table_and_data();
|
||||
if ( $products_exist ) {
|
||||
$this->enqueue_regeneration_step_run();
|
||||
} else {
|
||||
$this->finalize_regeneration();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a regeneration is already in progress.
|
||||
*
|
||||
* @return bool True if a regeneration is already in progress.
|
||||
*/
|
||||
public function regeneration_is_in_progress() {
|
||||
return ! is_null( get_option( 'woocommerce_attribute_lookup__last_products_page_processed', null ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the existing data related to the lookup table, including the table itself.
|
||||
*
|
||||
* Shortcut to run this method in case the debug tools UI isn't available or for quick debugging:
|
||||
*
|
||||
* wp eval "wc_get_container()->get(Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator::class)->delete_all_attributes_lookup_data();"
|
||||
*/
|
||||
public function delete_all_attributes_lookup_data() {
|
||||
global $wpdb;
|
||||
|
||||
delete_option( 'woocommerce_attribute_lookup__enabled' );
|
||||
delete_option( 'woocommerce_attribute_lookup__last_product_id_to_process' );
|
||||
delete_option( 'woocommerce_attribute_lookup__last_products_page_processed' );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS ' . $this->lookup_table_name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the lookup table and initialize the options that will be temporarily used
|
||||
* while the regeneration is in progress.
|
||||
*
|
||||
* @return bool True if there's any product at all in the database, false otherwise.
|
||||
*/
|
||||
private function initialize_table_and_data() {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
|
||||
$wpdb->query(
|
||||
'
|
||||
CREATE TABLE ' . $this->lookup_table_name . '(
|
||||
product_id bigint(20) NOT NULL,
|
||||
product_or_parent_id bigint(20) NOT NULL,
|
||||
taxonomy varchar(32) NOT NULL,
|
||||
term_id bigint(20) NOT NULL,
|
||||
is_variation_attribute tinyint(1) NOT NULL,
|
||||
in_stock tinyint(1) NOT NULL
|
||||
);
|
||||
'
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
$last_existing_product_id =
|
||||
WC()->call_function(
|
||||
'wc_get_products',
|
||||
array(
|
||||
'return' => 'ids',
|
||||
'limit' => 1,
|
||||
'orderby' => array(
|
||||
'ID' => 'DESC',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $last_existing_product_id ) {
|
||||
// No products exist, nothing to (re)generate.
|
||||
return false;
|
||||
}
|
||||
|
||||
update_option( 'woocommerce_attribute_lookup__last_product_id_to_process', current( $last_existing_product_id ) );
|
||||
update_option( 'woocommerce_attribute_lookup__last_products_page_processed', 0 );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action scheduler callback, performs one regeneration step and then
|
||||
* schedules the next step if necessary.
|
||||
*/
|
||||
private function run_regeneration_step_callback() {
|
||||
if ( ! $this->regeneration_is_in_progress() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->do_regeneration_step();
|
||||
if ( $result ) {
|
||||
$this->enqueue_regeneration_step_run();
|
||||
} else {
|
||||
$this->finalize_regeneration();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue one regeneration step in action scheduler.
|
||||
*/
|
||||
private function enqueue_regeneration_step_run() {
|
||||
$queue = WC()->get_instance_of( \WC_Queue::class );
|
||||
$queue->schedule_single(
|
||||
WC()->call_function( 'time' ) + 1,
|
||||
'woocommerce_run_product_attribute_lookup_update_callback',
|
||||
array(),
|
||||
'woocommerce-db-updates'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform one regeneration step: grabs a chunk of products and creates
|
||||
* the appropriate entries for them in the lookup table.
|
||||
*
|
||||
* @return bool True if more steps need to be run, false otherwise.
|
||||
*/
|
||||
private function do_regeneration_step() {
|
||||
$last_products_page_processed = get_option( 'woocommerce_attribute_lookup__last_products_page_processed' );
|
||||
$current_products_page = (int) $last_products_page_processed + 1;
|
||||
|
||||
$product_ids = WC()->call_function(
|
||||
'wc_get_products',
|
||||
array(
|
||||
'limit' => self::PRODUCTS_PER_GENERATION_STEP,
|
||||
'page' => $current_products_page,
|
||||
'orderby' => array(
|
||||
'ID' => 'ASC',
|
||||
),
|
||||
'return' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $product_ids ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $product_ids as $id ) {
|
||||
$this->data_store->update_data_for_product( $id );
|
||||
}
|
||||
|
||||
update_option( 'woocommerce_attribute_lookup__last_products_page_processed', $current_products_page );
|
||||
|
||||
$last_product_id_to_process = get_option( 'woocommerce_attribute_lookup__last_product_id_to_process' );
|
||||
return end( $product_ids ) < $last_product_id_to_process;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup/final option setup after the regeneration has been completed.
|
||||
*/
|
||||
private function finalize_regeneration() {
|
||||
delete_option( 'woocommerce_attribute_lookup__last_product_id_to_process' );
|
||||
delete_option( 'woocommerce_attribute_lookup__last_products_page_processed' );
|
||||
update_option( 'woocommerce_attribute_lookup__enabled', 'no' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the lookup table exists in the database.
|
||||
*
|
||||
* @return bool True if the lookup table exists in the database.
|
||||
*/
|
||||
private function lookup_table_exists() {
|
||||
global $wpdb;
|
||||
$query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->lookup_table_name ) );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
return $this->lookup_table_name === $wpdb->get_var( $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a 'Regenerate product attributes lookup table' entry to the Status - Tools page.
|
||||
*
|
||||
* @param array $tools_array The tool definitions array that is passed ro the woocommerce_debug_tools filter.
|
||||
* @return array The tools array with the entry added.
|
||||
*/
|
||||
private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ) {
|
||||
if ( ! $this->data_store->is_feature_visible() ) {
|
||||
return $tools_array;
|
||||
}
|
||||
|
||||
$lookup_table_exists = $this->lookup_table_exists();
|
||||
$generation_is_in_progress = $this->regeneration_is_in_progress();
|
||||
|
||||
// Regenerate table.
|
||||
|
||||
if ( $lookup_table_exists ) {
|
||||
$generate_item_name = __( 'Regenerate the product attributes lookup table', 'woocommerce' );
|
||||
$generate_item_desc = __( 'This tool will regenerate the product attributes lookup table data from existing products data. This process may take a while.', 'woocommerce' );
|
||||
$generate_item_return = __( 'Product attributes lookup table data is regenerating', 'woocommerce' );
|
||||
$generate_item_button = __( 'Regenerate', 'woocommerce' );
|
||||
} else {
|
||||
$generate_item_name = __( 'Create and fill product attributes lookup table', 'woocommerce' );
|
||||
$generate_item_desc = __( 'This tool will create the product attributes lookup table data and fill it with existing products data. This process may take a while.', 'woocommerce' );
|
||||
$generate_item_return = __( 'Product attributes lookup table is being filled', 'woocommerce' );
|
||||
$generate_item_button = __( 'Create', 'woocommerce' );
|
||||
}
|
||||
|
||||
$entry = array(
|
||||
'name' => $generate_item_name,
|
||||
'desc' => $generate_item_desc,
|
||||
'requires_refresh' => true,
|
||||
'callback' => function() use ( $generate_item_return ) {
|
||||
$this->initiate_regeneration_from_tools_page();
|
||||
return $generate_item_return;
|
||||
},
|
||||
);
|
||||
|
||||
if ( $generation_is_in_progress ) {
|
||||
$entry['button'] = sprintf(
|
||||
/* translators: %d: How many products have been processed so far. */
|
||||
__( 'Filling in progress (%d)', 'woocommerce' ),
|
||||
get_option( 'woocommerce_attribute_lookup__last_products_page_processed', 0 ) * self::PRODUCTS_PER_GENERATION_STEP
|
||||
);
|
||||
$entry['disabled'] = true;
|
||||
} else {
|
||||
$entry['button'] = $generate_item_button;
|
||||
}
|
||||
|
||||
$tools_array['regenerate_product_attributes_lookup_table'] = $entry;
|
||||
|
||||
if ( $lookup_table_exists ) {
|
||||
|
||||
// Delete the table.
|
||||
|
||||
$tools_array['delete_product_attributes_lookup_table'] = array(
|
||||
'name' => __( 'Delete the product attributes lookup table', 'woocommerce' ),
|
||||
'desc' => sprintf(
|
||||
'<strong class="red">%1$s</strong> %2$s',
|
||||
__( 'Note:', 'woocommerce' ),
|
||||
__( 'This will delete the product attributes lookup table. You can create it again with the "Create and fill product attributes lookup table" tool.', 'woocommerce' )
|
||||
),
|
||||
'button' => __( 'Delete', 'woocommerce' ),
|
||||
'requires_refresh' => true,
|
||||
'callback' => function () {
|
||||
$this->delete_all_attributes_lookup_data();
|
||||
return __( 'Product attributes lookup table has been deleted.', 'woocommerce' );
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if ( $lookup_table_exists && ! $generation_is_in_progress ) {
|
||||
|
||||
// Enable or disable table usage.
|
||||
|
||||
if ( 'yes' === get_option( 'woocommerce_attribute_lookup__enabled' ) ) {
|
||||
$tools_array['disable_product_attributes_lookup_table_usage'] = array(
|
||||
'name' => __( 'Disable the product attributes lookup table usage', 'woocommerce' ),
|
||||
'desc' => __( 'The product attributes lookup table usage is currently enabled, use this tool to disable it.', 'woocommerce' ),
|
||||
'button' => __( 'Disable', 'woocommerce' ),
|
||||
'requires_refresh' => true,
|
||||
'callback' => function () {
|
||||
$this->enable_or_disable_lookup_table_usage( false );
|
||||
return __( 'Product attributes lookup table usage has been disabled.', 'woocommerce' );
|
||||
},
|
||||
);
|
||||
} else {
|
||||
$tools_array['enable_product_attributes_lookup_table_usage'] = array(
|
||||
'name' => __( 'Enable the product attributes lookup table usage', 'woocommerce' ),
|
||||
'desc' => __( 'The product attributes lookup table usage is currently disabled, use this tool to enable it.', 'woocommerce' ),
|
||||
'button' => __( 'Enable', 'woocommerce' ),
|
||||
'requires_refresh' => true,
|
||||
'callback' => function () {
|
||||
$this->enable_or_disable_lookup_table_usage( true );
|
||||
return __( 'Product attributes lookup table usage has been enabled.', 'woocommerce' );
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tools_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to initiate the regeneration process from the Status - Tools page.
|
||||
*
|
||||
* @throws \Exception The regeneration is already in progress.
|
||||
*/
|
||||
private function initiate_regeneration_from_tools_page() {
|
||||
if ( $this->regeneration_is_in_progress() ) {
|
||||
throw new \Exception( 'Product attributes lookup table is already regenerating.' );
|
||||
}
|
||||
|
||||
$this->initiate_regeneration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the actual lookup table usage.
|
||||
*
|
||||
* @param bool $enable True to enable, false to disable.
|
||||
* @throws \Exception A lookup table regeneration is currently in progress.
|
||||
*/
|
||||
private function enable_or_disable_lookup_table_usage( $enable ) {
|
||||
if ( $this->regeneration_is_in_progress() ) {
|
||||
throw new \Exception( "Can't enable or disable the attributes lookup table usage while it's regenerating." );
|
||||
}
|
||||
|
||||
update_option( 'woocommerce_attribute_lookup__enabled', $enable ? 'yes' : 'no' );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,332 @@
|
|||
<?php
|
||||
/**
|
||||
* LookupDataStore class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
|
||||
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Data store class for the product attributes lookup table.
|
||||
*/
|
||||
class LookupDataStore {
|
||||
|
||||
/**
|
||||
* The lookup table name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $lookup_table_name;
|
||||
|
||||
/**
|
||||
* Is the feature visible?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_feature_visible;
|
||||
|
||||
/**
|
||||
* LookupDataStore constructor. Makes the feature hidden by default.
|
||||
*/
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
|
||||
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
|
||||
$this->is_feature_visible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the feature is visible (so that dedicated entries will be added to the debug tools page).
|
||||
*
|
||||
* @return bool True if the feature is visible.
|
||||
*/
|
||||
public function is_feature_visible() {
|
||||
return $this->is_feature_visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the feature visible, so that dedicated entries will be added to the debug tools page.
|
||||
*/
|
||||
public function show_feature() {
|
||||
$this->is_feature_visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the feature, so that no entries will be added to the debug tools page.
|
||||
*/
|
||||
public function hide_feature() {
|
||||
$this->is_feature_visible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update the lookup data for a given product or variation.
|
||||
* If a variable product is passed the information is updated for all of its variations.
|
||||
*
|
||||
* @param int|WC_Product $product Product object or id.
|
||||
* @throws \Exception A variation object is passed.
|
||||
*/
|
||||
public function update_data_for_product( $product ) {
|
||||
// TODO: For now data is always deleted and fully regenerated, existing data should be updated instead.
|
||||
|
||||
if ( ! is_a( $product, \WC_Product::class ) ) {
|
||||
$product = WC()->call_function( 'wc_get_product', $product );
|
||||
}
|
||||
|
||||
if ( $this->is_variation( $product ) ) {
|
||||
throw new \Exception( "LookupDataStore::update_data_for_product can't be called for variations." );
|
||||
}
|
||||
|
||||
$this->delete_lookup_table_entries_for( $product->get_id() );
|
||||
|
||||
if ( $this->is_variable_product( $product ) ) {
|
||||
$this->create_lookup_table_entries_for_variable_product( $product );
|
||||
} else {
|
||||
$this->create_lookup_table_entries_for_simple_product( $product );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the lookup table entries for a given product
|
||||
* (entries are identified by the "parent_or_product_id" field)
|
||||
*
|
||||
* @param int $product_id Simple product id, or main/parent product id for variable products.
|
||||
*/
|
||||
private function delete_lookup_table_entries_for( int $product_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'DELETE FROM ' . $this->lookup_table_name . ' WHERE product_or_parent_id = %d',
|
||||
$product_id
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Create lookup table entries for a simple (non variable) product.
|
||||
* Assumes that no entries exist yet.
|
||||
*
|
||||
* @param \WC_Product $product The product to create the entries for.
|
||||
*/
|
||||
private function create_lookup_table_entries_for_simple_product( \WC_Product $product ) {
|
||||
$product_attributes_data = $this->get_attribute_taxonomies( $product );
|
||||
$has_stock = $product->is_in_stock();
|
||||
$product_id = $product->get_id();
|
||||
foreach ( $product_attributes_data as $taxonomy => $data ) {
|
||||
$term_ids = $data['term_ids'];
|
||||
foreach ( $term_ids as $term_id ) {
|
||||
$this->insert_lookup_table_data( $product_id, $product_id, $taxonomy, $term_id, false, $has_stock );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create lookup table entries for a variable product.
|
||||
* Assumes that no entries exist yet.
|
||||
*
|
||||
* @param \WC_Product_Variable $product The product to create the entries for.
|
||||
*/
|
||||
private function create_lookup_table_entries_for_variable_product( \WC_Product_Variable $product ) {
|
||||
$product_attributes_data = $this->get_attribute_taxonomies( $product );
|
||||
$variation_attributes_data = array_filter(
|
||||
$product_attributes_data,
|
||||
function( $item ) {
|
||||
return $item['used_for_variations'];
|
||||
}
|
||||
);
|
||||
$non_variation_attributes_data = array_filter(
|
||||
$product_attributes_data,
|
||||
function( $item ) {
|
||||
return ! $item['used_for_variations'];
|
||||
}
|
||||
);
|
||||
|
||||
$main_product_has_stock = $product->is_in_stock();
|
||||
$main_product_id = $product->get_id();
|
||||
|
||||
foreach ( $non_variation_attributes_data as $taxonomy => $data ) {
|
||||
$term_ids = $data['term_ids'];
|
||||
foreach ( $term_ids as $term_id ) {
|
||||
$this->insert_lookup_table_data( $main_product_id, $main_product_id, $taxonomy, $term_id, false, $main_product_has_stock );
|
||||
}
|
||||
}
|
||||
|
||||
$term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );
|
||||
$variations = $this->get_variations_of( $product );
|
||||
|
||||
foreach ( $variation_attributes_data as $taxonomy => $data ) {
|
||||
foreach ( $variations as $variation ) {
|
||||
$variation_id = $variation->get_id();
|
||||
$variation_has_stock = $variation->is_in_stock();
|
||||
$variation_definition_term_id = $this->get_variation_definition_term_id( $variation, $taxonomy, $term_ids_by_slug_cache );
|
||||
if ( $variation_definition_term_id ) {
|
||||
$this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $variation_definition_term_id, true, $variation_has_stock );
|
||||
} else {
|
||||
$term_ids_for_taxonomy = $data['term_ids'];
|
||||
foreach ( $term_ids_for_taxonomy as $term_id ) {
|
||||
$this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $term_id, true, $variation_has_stock );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cache of term ids by slug for a set of taxonomies, with this format:
|
||||
*
|
||||
* [
|
||||
* 'taxonomy' => [
|
||||
* 'slug_1' => id_1,
|
||||
* 'slug_2' => id_2,
|
||||
* ...
|
||||
* ], ...
|
||||
* ]
|
||||
*
|
||||
* @param array $taxonomies List of taxonomies to build the cache for.
|
||||
* @return array A dictionary of taxonomies => dictionary of term slug => term id.
|
||||
*/
|
||||
private function get_term_ids_by_slug_cache( $taxonomies ) {
|
||||
$result = array();
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
$terms = WC()->call_function(
|
||||
'get_terms',
|
||||
array(
|
||||
'taxonomy' => $taxonomy,
|
||||
'hide_empty' => false,
|
||||
'fields' => 'id=>slug',
|
||||
)
|
||||
);
|
||||
$result[ $taxonomy ] = array_flip( $terms );
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id of the term that defines a variation for a given taxonomy,
|
||||
* or null if there's no such defining id (for variations having "Any <taxonomy>" as the definition)
|
||||
*
|
||||
* @param \WC_Product_Variation $variation The variation to get the defining term id for.
|
||||
* @param string $taxonomy The taxonomy to get the defining term id for.
|
||||
* @param array $term_ids_by_slug_cache A term ids by slug as generated by get_term_ids_by_slug_cache.
|
||||
* @return int|null The term id, or null if there's no defining id for that taxonomy in that variation.
|
||||
*/
|
||||
private function get_variation_definition_term_id( \WC_Product_Variation $variation, string $taxonomy, array $term_ids_by_slug_cache ) {
|
||||
$variation_attributes = $variation->get_attributes();
|
||||
$term_slug = ArrayUtil::get_value_or_default( $variation_attributes, $taxonomy );
|
||||
if ( $term_slug ) {
|
||||
return $term_ids_by_slug_cache[ $taxonomy ][ $term_slug ];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the variations of a given variable product.
|
||||
*
|
||||
* @param \WC_Product_Variable $product The product to get the variations for.
|
||||
* @return array An array of WC_Product_Variation objects.
|
||||
*/
|
||||
private function get_variations_of( \WC_Product_Variable $product ) {
|
||||
$variation_ids = $product->get_children();
|
||||
return array_map(
|
||||
function( $id ) {
|
||||
return WC()->call_function( 'wc_get_product', $id );
|
||||
},
|
||||
$variation_ids
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given product is a variable product.
|
||||
*
|
||||
* @param \WC_Product $product The product to check.
|
||||
* @return bool True if it's a variable product, false otherwise.
|
||||
*/
|
||||
private function is_variable_product( \WC_Product $product ) {
|
||||
return is_a( $product, \WC_Product_Variable::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given product is a variation.
|
||||
*
|
||||
* @param \WC_Product $product The product to check.
|
||||
* @return bool True if it's a variation, false otherwise.
|
||||
*/
|
||||
private function is_variation( \WC_Product $product ) {
|
||||
return is_a( $product, \WC_Product_Variation::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of taxonomies used for variations on a product together with
|
||||
* the associated term ids, with the following format:
|
||||
*
|
||||
* [
|
||||
* 'taxonomy_name' =>
|
||||
* [
|
||||
* 'term_ids' => [id, id, ...],
|
||||
* 'used_for_variations' => true|false
|
||||
* ], ...
|
||||
* ]
|
||||
*
|
||||
* @param \WC_Product $product The product to get the attribute taxonomies for.
|
||||
* @return array Information about the attribute taxonomies of the product.
|
||||
*/
|
||||
private function get_attribute_taxonomies( \WC_Product $product ) {
|
||||
$product_attributes = $product->get_attributes();
|
||||
$result = array();
|
||||
foreach ( $product_attributes as $taxonomy_name => $attribute_data ) {
|
||||
if ( ! $attribute_data->get_id() ) {
|
||||
// Custom product attribute, not suitable for attribute-based filtering.
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[ $taxonomy_name ] = array(
|
||||
'term_ids' => $attribute_data->get_options(),
|
||||
'used_for_variations' => $attribute_data->get_variation(),
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert one entry in the lookup table.
|
||||
*
|
||||
* @param int $product_id The product id.
|
||||
* @param int $product_or_parent_id The product id for non-variable products, the main/parent product id for variations.
|
||||
* @param string $taxonomy Taxonomy name.
|
||||
* @param int $term_id Term id.
|
||||
* @param bool $is_variation_attribute True if the taxonomy corresponds to an attribute used to define variations.
|
||||
* @param bool $has_stock True if the product is in stock.
|
||||
*/
|
||||
private function insert_lookup_table_data( int $product_id, int $product_or_parent_id, string $taxonomy, int $term_id, bool $is_variation_attribute, bool $has_stock ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
'INSERT INTO ' . $this->lookup_table_name . ' (
|
||||
product_id,
|
||||
product_or_parent_id,
|
||||
taxonomy,
|
||||
term_id,
|
||||
is_variation_attribute,
|
||||
in_stock)
|
||||
VALUES
|
||||
( %d, %d, %s, %d, %d, %d )',
|
||||
$product_id,
|
||||
$product_or_parent_id,
|
||||
$taxonomy,
|
||||
$term_id,
|
||||
$is_variation_attribute ? 1 : 0,
|
||||
$has_stock ? 1 : 0
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
}
|
|
@ -44,5 +44,28 @@ class ArrayUtil {
|
|||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given key exists in an array and its value can be evaluated as 'true'.
|
||||
*
|
||||
* @param array $array The array to check.
|
||||
* @param string $key The key for the value to check.
|
||||
* @return bool True if the key exists in the array and the value can be evaluated as 'true'.
|
||||
*/
|
||||
public static function is_truthy( array $array, string $key ) {
|
||||
return isset( $array[ $key ] ) && $array[ $key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value for a given key from an array, or a default value if the key doesn't exist in the array.
|
||||
*
|
||||
* @param array $array The array to get the value from.
|
||||
* @param string $key The key to use to retrieve the value.
|
||||
* @param null $default The default value to return if the key doesn't exist in the array.
|
||||
* @return mixed|null The value for the key, or the default value passed.
|
||||
*/
|
||||
public static function get_value_or_default( array $array, string $key, $default = null ) {
|
||||
return isset( $array[ $key ] ) ? $array[ $key ] : $default;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
/**
|
||||
* FakeQueue class file.
|
||||
*
|
||||
* @package WooCommerce\Testing\Tools
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Testing\Tools;
|
||||
|
||||
/**
|
||||
* Fake scheduled actions queue for unit tests, it just records all the method calls
|
||||
* in a publicly accessible $methods_called property.
|
||||
*
|
||||
* To use:
|
||||
*
|
||||
* 1. The production class must get an instance of the queue in this way:
|
||||
*
|
||||
* WC()->get_instance_of(\WC_Queue::class)
|
||||
*
|
||||
* 2. Add the following in the setUp() method of the unit tests class:
|
||||
*
|
||||
* $this->register_legacy_proxy_class_mocks([\WC_Queue::class => new FakeQueue()]);
|
||||
*
|
||||
* 3. Get the instance of the fake queue with $this->get_legacy_instance_of(\WC_Queue::class)
|
||||
* and check its methods_called field as appropriate.
|
||||
*/
|
||||
class FakeQueue implements \WC_Queue_Interface {
|
||||
|
||||
/**
|
||||
* Records all the method calls to this instance.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $methods_called = array();
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionComment.Missing
|
||||
|
||||
public function add( $hook, $args = array(), $group = '' ) {
|
||||
// TODO: Implement add() method.
|
||||
}
|
||||
|
||||
public function schedule_single( $timestamp, $hook, $args = array(), $group = '' ) {
|
||||
$this->add_to_methods_called(
|
||||
'schedule_single',
|
||||
$args,
|
||||
$group,
|
||||
array(
|
||||
'timestamp' => $timestamp,
|
||||
'hook' => $hook,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) {
|
||||
// TODO: Implement schedule_recurring() method.
|
||||
}
|
||||
|
||||
public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' ) {
|
||||
// TODO: Implement schedule_cron() method.
|
||||
}
|
||||
|
||||
public function cancel( $hook, $args = array(), $group = '' ) {
|
||||
// TODO: Implement cancel() method.
|
||||
}
|
||||
|
||||
public function cancel_all( $hook, $args = array(), $group = '' ) {
|
||||
// TODO: Implement cancel_all() method.
|
||||
}
|
||||
|
||||
public function get_next( $hook, $args = null, $group = '' ) {
|
||||
// TODO: Implement get_next() method.
|
||||
}
|
||||
|
||||
public function search( $args = array(), $return_format = OBJECT ) {
|
||||
// TODO: Implement search() method.
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionComment.Missing
|
||||
|
||||
/**
|
||||
* Registers a method call for this instance.
|
||||
*
|
||||
* @param string $method Name of the invoked method.
|
||||
* @param array $args Arguments passed in '$args' to the method call.
|
||||
* @param string $group Group name passed in '$group' to the method call.
|
||||
* @param array $extra_args Any extra information to store about the method call.
|
||||
*/
|
||||
private function add_to_methods_called( $method, $args, $group, $extra_args = array() ) {
|
||||
$value = array(
|
||||
'method' => $method,
|
||||
'args' => $args,
|
||||
'group' => $group,
|
||||
);
|
||||
|
||||
$this->methods_called[] = array_merge( $value, $extra_args );
|
||||
}
|
||||
}
|
|
@ -103,6 +103,20 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
|
|||
$this->assertSame( $instance_2, $this->sut->get( DependencyClass::class ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox 'replace' should allow to replace existing registrations with anonymous classes.
|
||||
*/
|
||||
public function test_replace_allows_replacing_existing_registrations_with_anonymous_classes() {
|
||||
$instance_1 = new DependencyClass();
|
||||
$instance_2 = new class() extends DependencyClass {};
|
||||
|
||||
$this->sut->add( DependencyClass::class, $instance_1, true );
|
||||
$this->assertSame( $instance_1, $this->sut->get( DependencyClass::class ) );
|
||||
|
||||
$this->sut->replace( DependencyClass::class, $instance_2, true );
|
||||
$this->assertSame( $instance_2, $this->sut->get( DependencyClass::class ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox 'reset_all_resolved' should discard cached resolutions for classes registered as 'shared'.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
<?php
|
||||
/**
|
||||
* DataRegeneratorTest class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Tests\Internal\ProductAttributesLookup;
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
use Automattic\WooCommerce\Testing\Tools\FakeQueue;
|
||||
|
||||
/**
|
||||
* Tests for the DataRegenerator class.
|
||||
* @package Automattic\WooCommerce\Tests\Internal\ProductAttributesLookup
|
||||
*/
|
||||
class DataRegeneratorTest extends \WC_Unit_Test_Case {
|
||||
|
||||
/**
|
||||
* The system under test.
|
||||
*
|
||||
* @var DataRegenerator
|
||||
*/
|
||||
private $sut;
|
||||
|
||||
/**
|
||||
* @var LookupDataStore
|
||||
*/
|
||||
private $lookup_data_store;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $lookup_table_name;
|
||||
|
||||
/**
|
||||
* @var FakeQueue
|
||||
*/
|
||||
private $queue;
|
||||
|
||||
/**
|
||||
* Runs before each test.
|
||||
*/
|
||||
public function setUp() {
|
||||
global $wpdb;
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
|
||||
|
||||
// phpcs:disable Squiz.Commenting
|
||||
$this->lookup_data_store = new class() extends LookupDataStore {
|
||||
public $passed_products = array();
|
||||
|
||||
public function update_data_for_product( $product ) {
|
||||
$this->passed_products[] = $product;
|
||||
}
|
||||
};
|
||||
// phpcs:enable Squiz.Commenting
|
||||
|
||||
// This is needed to prevent the hook to act on the already registered LookupDataStore class.
|
||||
remove_all_actions( 'woocommerce_run_product_attribute_lookup_update_callback' );
|
||||
|
||||
$container = wc_get_container();
|
||||
$container->reset_all_resolved();
|
||||
$container->replace( LookupDataStore::class, $this->lookup_data_store );
|
||||
$this->sut = $container->get( DataRegenerator::class );
|
||||
|
||||
$this->register_legacy_proxy_class_mocks(
|
||||
array(
|
||||
\WC_Queue::class => new FakeQueue(),
|
||||
)
|
||||
);
|
||||
$this->queue = $this->get_legacy_instance_of( \WC_Queue::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `initiate_regeneration` creates the lookup table, deleting it first if it already existed.
|
||||
*
|
||||
* @testWith [false]
|
||||
* [true]
|
||||
*
|
||||
* @param bool $previously_existing True to create a lookup table beforehand.
|
||||
*/
|
||||
public function test_initiate_regeneration_creates_looukp_table( $previously_existing ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS ' . $this->lookup_table_name );
|
||||
|
||||
if ( $previously_existing ) {
|
||||
$wpdb->query( 'CREATE TABLE ' . $this->lookup_table_name . ' (foo int);' );
|
||||
}
|
||||
|
||||
$this->sut->initiate_regeneration();
|
||||
|
||||
// Try to insert a row to verify that the table exists.
|
||||
// We can't use the regular table existence detection mechanisms because PHPUnit creates all tables as temporary.
|
||||
$wpdb->query( 'INSERT INTO ' . $this->lookup_table_name . " VALUES (1, 1, 'taxonomy', 1, 1, 1 )" );
|
||||
$value = $wpdb->get_var( 'SELECT product_id FROM ' . $this->lookup_table_name . ' LIMIT 1' );
|
||||
$this->assertEquals( 1, $value );
|
||||
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `initiate_regeneration` initializes the transient options, and enqueues the first step for time()+1.
|
||||
*/
|
||||
public function test_initiate_regeneration_initializes_temporary_options_and_enqueues_regeneration_step() {
|
||||
$this->register_legacy_proxy_function_mocks(
|
||||
array(
|
||||
'wc_get_products' => function( $args ) {
|
||||
return array( 100 );
|
||||
},
|
||||
'time' => function() {
|
||||
return 1000;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
$this->sut->initiate_regeneration();
|
||||
|
||||
$this->assertEquals( 100, get_option( 'woocommerce_attribute_lookup__last_product_id_to_process' ) );
|
||||
$this->assertEquals( 0, get_option( 'woocommerce_attribute_lookup__last_products_page_processed' ) );
|
||||
$this->assertFalse( get_option( 'woocommerce_attribute_lookup__enabled' ) );
|
||||
|
||||
$expected_enqueued = array(
|
||||
'method' => 'schedule_single',
|
||||
'args' => array(),
|
||||
'timestamp' => 1001,
|
||||
'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
|
||||
'group' => 'woocommerce-db-updates',
|
||||
);
|
||||
$actual_enqueued = current( $this->queue->methods_called );
|
||||
|
||||
$this->assertEquals( sort( $expected_enqueued ), sort( $actual_enqueued ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `initiate_regeneration` finalizes the regeneration process without enqueueing any step if the db is empty.
|
||||
*
|
||||
* @testWith [false]
|
||||
* [[]]
|
||||
*
|
||||
* @param mixed $get_products_result Result from wc_get_products.
|
||||
*/
|
||||
public function test_initiate_regeneration_does_not_enqueues_regeneration_step_when_no_products( $get_products_result ) {
|
||||
$this->register_legacy_proxy_function_mocks(
|
||||
array(
|
||||
'wc_get_products' => function( $args ) use ( $get_products_result ) {
|
||||
return $get_products_result;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
$this->sut->initiate_regeneration();
|
||||
|
||||
$this->assertFalse( get_option( 'woocommerce_attribute_lookup__last_product_id_to_process' ) );
|
||||
$this->assertFalse( get_option( 'woocommerce_attribute_lookup__last_products_page_processed' ) );
|
||||
$this->assertEquals( 'no', get_option( 'woocommerce_attribute_lookup__enabled' ) );
|
||||
$this->assertEmpty( $this->queue->methods_called );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `initiate_regeneration` processes one chunk of products IDs and enqueues next step if there are more products available.
|
||||
*/
|
||||
public function test_initiate_regeneration_correctly_processes_ids_and_enqueues_next_step() {
|
||||
$requested_products_pages = array();
|
||||
|
||||
$this->register_legacy_proxy_function_mocks(
|
||||
array(
|
||||
'wc_get_products' => function( $args ) use ( &$requested_products_pages ) {
|
||||
if ( 'DESC' === current( $args['orderby'] ) ) {
|
||||
return array( 100 );
|
||||
} else {
|
||||
$requested_products_pages[] = $args['page'];
|
||||
return array( 1, 2, 3 );
|
||||
}
|
||||
},
|
||||
'time' => function() {
|
||||
return 1000;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
$this->sut->initiate_regeneration();
|
||||
$this->queue->methods_called = array();
|
||||
|
||||
update_option( 'woocommerce_attribute_lookup__last_products_page_processed', 7 );
|
||||
|
||||
do_action( 'woocommerce_run_product_attribute_lookup_update_callback' );
|
||||
|
||||
$this->assertEquals( array( 1, 2, 3 ), $this->lookup_data_store->passed_products );
|
||||
$this->assertEquals( array( 8 ), $requested_products_pages );
|
||||
$this->assertEquals( 8, get_option( 'woocommerce_attribute_lookup__last_products_page_processed' ) );
|
||||
|
||||
$expected_enqueued = array(
|
||||
'method' => 'schedule_single',
|
||||
'args' => array(),
|
||||
'timestamp' => 1001,
|
||||
'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
|
||||
'group' => 'woocommerce-db-updates',
|
||||
);
|
||||
$actual_enqueued = current( $this->queue->methods_called );
|
||||
$this->assertEquals( sort( $expected_enqueued ), sort( $actual_enqueued ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `initiate_regeneration` finishes regeneration when the max product id is reached or no more products are returned.
|
||||
*
|
||||
* @testWith [[98,99,100]]
|
||||
* [[99,100,101]]
|
||||
* [[]]
|
||||
*
|
||||
* @param array $product_ids The products ids that wc_get_products will return.
|
||||
*/
|
||||
public function test_initiate_regeneration_finishes_when_no_more_products_available( $product_ids ) {
|
||||
$requested_products_pages = array();
|
||||
|
||||
$this->register_legacy_proxy_function_mocks(
|
||||
array(
|
||||
'wc_get_products' => function( $args ) use ( &$requested_products_pages, $product_ids ) {
|
||||
if ( 'DESC' === current( $args['orderby'] ) ) {
|
||||
return array( 100 );
|
||||
} else {
|
||||
$requested_products_pages[] = $args['page'];
|
||||
return $product_ids;
|
||||
}
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
$this->sut->initiate_regeneration();
|
||||
$this->queue->methods_called = array();
|
||||
|
||||
do_action( 'woocommerce_run_product_attribute_lookup_update_callback' );
|
||||
|
||||
$this->assertEquals( $product_ids, $this->lookup_data_store->passed_products );
|
||||
$this->assertFalse( get_option( 'woocommerce_attribute_lookup__last_product_id_to_process' ) );
|
||||
$this->assertFalse( get_option( 'woocommerce_attribute_lookup__last_products_page_processed' ) );
|
||||
$this->assertEquals( 'no', get_option( 'woocommerce_attribute_lookup__enabled' ) );
|
||||
$this->assertEmpty( $this->queue->methods_called );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,383 @@
|
|||
<?php
|
||||
/**
|
||||
* LookupDataStoreTest class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Tests\Internal\ProductAttributesLookup;
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
use Automattic\WooCommerce\Testing\Tools\FakeQueue;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
|
||||
/**
|
||||
* Tests for the LookupDataStore class.
|
||||
* @package Automattic\WooCommerce\Tests\Internal\ProductAttributesLookup
|
||||
*/
|
||||
class LookupDataStoreTest extends \WC_Unit_Test_Case {
|
||||
|
||||
/**
|
||||
* The system under test.
|
||||
*
|
||||
* @var LookupDataStore
|
||||
*/
|
||||
private $sut;
|
||||
|
||||
/**
|
||||
* Runs before each test.
|
||||
*/
|
||||
public function setUp() {
|
||||
global $wpdb;
|
||||
|
||||
$this->sut = new LookupDataStore();
|
||||
|
||||
// Initiating regeneration with a fake queue will just create the lookup table in the database.
|
||||
add_filter(
|
||||
'woocommerce_queue_class',
|
||||
function() {
|
||||
return FakeQueue::class;
|
||||
}
|
||||
);
|
||||
$this->get_instance_of( DataRegenerator::class )->initiate_regeneration();
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `test_update_data_for_product` throws an exception if a variation is passed.
|
||||
*/
|
||||
public function test_update_data_for_product_throws_if_variation_is_passed() {
|
||||
$product = new \WC_Product_Variation();
|
||||
|
||||
$this->expectException( \Exception::class );
|
||||
$this->expectExceptionMessage( "LookupDataStore::update_data_for_product can't be called for variations." );
|
||||
|
||||
$this->sut->update_data_for_product( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `test_update_data_for_product` creates the appropriate entries for simple products, skipping custom product attributes.
|
||||
*
|
||||
* @testWith [true]
|
||||
* [false]
|
||||
*
|
||||
* @param bool $in_stock 'true' if the product is supposed to be in stock.
|
||||
*/
|
||||
public function test_update_data_for_simple_product( $in_stock ) {
|
||||
$product = new \WC_Product_Simple();
|
||||
$product->set_id( 10 );
|
||||
$this->set_product_attributes(
|
||||
$product,
|
||||
array(
|
||||
'pa_attribute_1' => array(
|
||||
'id' => 100,
|
||||
'options' => array( 51, 52 ),
|
||||
),
|
||||
'pa_attribute_2' => array(
|
||||
'id' => 200,
|
||||
'options' => array( 73, 74 ),
|
||||
),
|
||||
'pa_custom_attribute' => array(
|
||||
'id' => 0,
|
||||
'options' => array( 'foo', 'bar' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $in_stock ) {
|
||||
$product->set_stock_status( 'instock' );
|
||||
$expected_in_stock = 1;
|
||||
} else {
|
||||
$product->set_stock_status( 'outofstock' );
|
||||
$expected_in_stock = 0;
|
||||
}
|
||||
|
||||
$this->sut->update_data_for_product( $product );
|
||||
|
||||
$expected = array(
|
||||
array(
|
||||
'product_id' => 10,
|
||||
'product_or_parent_id' => 10,
|
||||
'taxonomy' => 'pa_attribute_1',
|
||||
'term_id' => 51,
|
||||
'in_stock' => $expected_in_stock,
|
||||
'is_variation_attribute' => 0,
|
||||
),
|
||||
array(
|
||||
'product_id' => 10,
|
||||
'product_or_parent_id' => 10,
|
||||
'taxonomy' => 'pa_attribute_1',
|
||||
'term_id' => 52,
|
||||
'in_stock' => $expected_in_stock,
|
||||
'is_variation_attribute' => 0,
|
||||
),
|
||||
array(
|
||||
'product_id' => 10,
|
||||
'product_or_parent_id' => 10,
|
||||
'taxonomy' => 'pa_attribute_2',
|
||||
'term_id' => 73,
|
||||
'in_stock' => $expected_in_stock,
|
||||
'is_variation_attribute' => 0,
|
||||
),
|
||||
array(
|
||||
'product_id' => 10,
|
||||
'product_or_parent_id' => 10,
|
||||
'taxonomy' => 'pa_attribute_2',
|
||||
'term_id' => 74,
|
||||
'in_stock' => $expected_in_stock,
|
||||
'is_variation_attribute' => 0,
|
||||
),
|
||||
);
|
||||
|
||||
$actual = $this->get_lookup_table_data();
|
||||
|
||||
$this->assertEquals( sort( $expected ), sort( $actual ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `test_update_data_for_product` creates the appropriate entries for variable products.
|
||||
*/
|
||||
public function test_update_data_for_variable_product() {
|
||||
$products = array();
|
||||
|
||||
/**
|
||||
* Create one normal attribute and two attributes used to define variations,
|
||||
* with 4 terms each.
|
||||
*/
|
||||
|
||||
$this->register_legacy_proxy_function_mocks(
|
||||
array(
|
||||
'get_terms' => function( $args ) use ( &$invokations_of_get_terms ) {
|
||||
switch ( $args['taxonomy'] ) {
|
||||
case 'non-variation-attribute':
|
||||
return array(
|
||||
10 => 'term_10',
|
||||
20 => 'term_20',
|
||||
30 => 'term_30',
|
||||
40 => 'term_40',
|
||||
);
|
||||
case 'variation-attribute-1':
|
||||
return array(
|
||||
50 => 'term_50',
|
||||
60 => 'term_60',
|
||||
70 => 'term_70',
|
||||
80 => 'term_80',
|
||||
);
|
||||
case 'variation-attribute-2':
|
||||
return array(
|
||||
90 => 'term_90',
|
||||
100 => 'term_100',
|
||||
110 => 'term_110',
|
||||
120 => 'term_120',
|
||||
);
|
||||
default:
|
||||
throw new \Exception( "Unexpected call to 'get_terms'" );
|
||||
}
|
||||
},
|
||||
'wc_get_product' => function( $id ) use ( &$products ) {
|
||||
return $products[ $id ];
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a variable product with:
|
||||
* - 3 of the 4 values of the regular attribute.
|
||||
* - A custom product attribute.
|
||||
* - The two variation attributes, with 3 of the 4 terms for each one.
|
||||
* - Variation 1 having one value for each of the variation attributes.
|
||||
* - Variation 2 having one value for variation-attribute-1
|
||||
* but none for variation-attribute-2 (so the value for that one is "Any").
|
||||
*/
|
||||
|
||||
$product = new \WC_Product_Variable();
|
||||
$product->set_id( 1000 );
|
||||
$this->set_product_attributes(
|
||||
$product,
|
||||
array(
|
||||
'non-variation-attribute' => array(
|
||||
'id' => 100,
|
||||
'options' => array( 10, 20, 30 ),
|
||||
),
|
||||
'pa_custom_attribute' => array(
|
||||
'id' => 0,
|
||||
'options' => array( 'foo', 'bar' ),
|
||||
),
|
||||
'variation-attribute-1' => array(
|
||||
'id' => 200,
|
||||
'options' => array( 50, 60, 70 ),
|
||||
'variation' => true,
|
||||
),
|
||||
'variation-attribute-2' => array(
|
||||
'id' => 300,
|
||||
'options' => array( 90, 100, 110 ),
|
||||
'variation' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
$product->set_stock_status( 'instock' );
|
||||
|
||||
$variation_1 = new \WC_Product_Variation();
|
||||
$variation_1->set_id( 1001 );
|
||||
$variation_1->set_attributes(
|
||||
array(
|
||||
'variation-attribute-1' => 'term_50',
|
||||
'variation-attribute-2' => 'term_90',
|
||||
)
|
||||
);
|
||||
$variation_1->set_stock_status( 'instock' );
|
||||
|
||||
$variation_2 = new \WC_Product_Variation();
|
||||
$variation_2->set_id( 1002 );
|
||||
$variation_2->set_attributes(
|
||||
array(
|
||||
'variation-attribute-1' => 'term_60',
|
||||
)
|
||||
);
|
||||
$variation_2->set_stock_status( 'outofstock' );
|
||||
|
||||
$product->set_children( array( 1001, 1002 ) );
|
||||
$products[1000] = $product;
|
||||
$products[1001] = $variation_1;
|
||||
$products[1002] = $variation_2;
|
||||
|
||||
$this->sut->update_data_for_product( $product );
|
||||
|
||||
$expected = array(
|
||||
// Main product: one entry for each of the regular attribute values,
|
||||
// excluding custom product attributes.
|
||||
|
||||
array(
|
||||
'product_id' => '1000',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'non-variation-attribute',
|
||||
'term_id' => '10',
|
||||
'is_variation_attribute' => '0',
|
||||
'in_stock' => '1',
|
||||
),
|
||||
array(
|
||||
'product_id' => '1000',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'non-variation-attribute',
|
||||
'term_id' => '20',
|
||||
'is_variation_attribute' => '0',
|
||||
'in_stock' => '1',
|
||||
),
|
||||
array(
|
||||
'product_id' => '1000',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'non-variation-attribute',
|
||||
'term_id' => '30',
|
||||
'is_variation_attribute' => '0',
|
||||
'in_stock' => '1',
|
||||
),
|
||||
|
||||
// Variation 1: one entry for each of the defined variation attributes.
|
||||
|
||||
array(
|
||||
'product_id' => '1001',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'variation-attribute-1',
|
||||
'term_id' => '50',
|
||||
'is_variation_attribute' => '1',
|
||||
'in_stock' => '1',
|
||||
),
|
||||
array(
|
||||
'product_id' => '1001',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'variation-attribute-2',
|
||||
'term_id' => '90',
|
||||
'is_variation_attribute' => '1',
|
||||
'in_stock' => '1',
|
||||
),
|
||||
|
||||
// Variation 2: one entry for the defined value for variation-attribute-1,
|
||||
// then one for each of the possible values of variation-attribute-2
|
||||
// (the values defined in the parent product).
|
||||
|
||||
array(
|
||||
'product_id' => '1002',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'variation-attribute-1',
|
||||
'term_id' => '60',
|
||||
'is_variation_attribute' => '1',
|
||||
'in_stock' => '0',
|
||||
),
|
||||
array(
|
||||
'product_id' => '1002',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'variation-attribute-2',
|
||||
'term_id' => '90',
|
||||
'is_variation_attribute' => '1',
|
||||
'in_stock' => '0',
|
||||
),
|
||||
array(
|
||||
'product_id' => '1002',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'variation-attribute-2',
|
||||
'term_id' => '100',
|
||||
'is_variation_attribute' => '1',
|
||||
'in_stock' => '0',
|
||||
),
|
||||
array(
|
||||
'product_id' => '1002',
|
||||
'product_or_parent_id' => '1000',
|
||||
'taxonomy' => 'variation-attribute-2',
|
||||
'term_id' => '110',
|
||||
'is_variation_attribute' => '1',
|
||||
'in_stock' => '0',
|
||||
),
|
||||
);
|
||||
|
||||
$actual = $this->get_lookup_table_data();
|
||||
$this->assertEquals( sort( $expected ), sort( $actual ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the product attributes from an array with this format:
|
||||
*
|
||||
* [
|
||||
* 'taxonomy_or_custom_attribute_name' =>
|
||||
* [
|
||||
* 'id' => attribute id (0 for custom product attribute),
|
||||
* 'options' => [term_id, term_id...] (for custom product attributes: ['term', 'term'...]
|
||||
* 'variation' => 1|0 (optional, default 0)
|
||||
* ], ...
|
||||
* ]
|
||||
*
|
||||
* @param WC_Product $product The product to set the attributes.
|
||||
* @param array $attributes_data The attributes to set.
|
||||
*/
|
||||
private function set_product_attributes( $product, $attributes_data ) {
|
||||
$attributes = array();
|
||||
foreach ( $attributes_data as $taxonomy => $attribute_data ) {
|
||||
$attribute = new \WC_Product_Attribute();
|
||||
$attribute->set_id( $attribute_data['id'] );
|
||||
$attribute->set_name( $taxonomy );
|
||||
$attribute->set_options( $attribute_data['options'] );
|
||||
$attribute->set_variation( ArrayUtil::get_value_or_default( $attribute_data, 'variation', false ) );
|
||||
$attributes[] = $attribute;
|
||||
}
|
||||
|
||||
$product->set_attributes( $attributes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the data in the lookup table as an array of associative arrays.
|
||||
*
|
||||
* @return array All the rows in the lookup table as an array of associative arrays.
|
||||
*/
|
||||
private function get_lookup_table_data() {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->get_results( 'select * from ' . $wpdb->prefix . 'wc_product_attributes_lookup', ARRAY_A );
|
||||
|
||||
foreach ( $result as $row ) {
|
||||
foreach ( $row as $column_name => $value ) {
|
||||
if ( 'taxonomy' !== $column_name ) {
|
||||
$row[ $column_name ] = (int) $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ use Automattic\WooCommerce\Utilities\ArrayUtil;
|
|||
* A collection of tests for the array utility class.
|
||||
*/
|
||||
class ArrayUtilTest extends \WC_Unit_Test_Case {
|
||||
|
||||
/**
|
||||
* @testdox `get_nested_value` should return null if the requested key doesn't exist and no default value is supplied.
|
||||
*
|
||||
|
@ -63,4 +64,74 @@ class ArrayUtilTest extends \WC_Unit_Test_Case {
|
|||
|
||||
$this->assertEquals( 'buzz', $actual );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `is_truthy` returns false when the key does not exist in the array.
|
||||
*/
|
||||
public function test_is_truthy_returns_false_if_key_does_not_exist() {
|
||||
$array = array( 'foo' => 'bar' );
|
||||
|
||||
$this->assertFalse( ArrayUtil::is_truthy( $array, 'fizz' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `is_truthy` returns false for values that evaluate to false.
|
||||
*
|
||||
* @testWith [0]
|
||||
* ["0"]
|
||||
* [false]
|
||||
* [""]
|
||||
* [null]
|
||||
* [[]]
|
||||
*
|
||||
* @param mixed $value Value to test.
|
||||
*/
|
||||
public function test_is_truthy_returns_false_if_value_evaluates_to_false( $value ) {
|
||||
$array = array( 'foo' => $value );
|
||||
|
||||
$this->assertFalse( ArrayUtil::is_truthy( $array, 'foo' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `is_truthy` returns true for values that evaluate to true.
|
||||
*
|
||||
* @testWith [1]
|
||||
* ["foo"]
|
||||
* [true]
|
||||
* [[1]]
|
||||
*
|
||||
* @param mixed $value Value to test.
|
||||
*/
|
||||
public function test_is_truthy_returns_false_if_value_evaluates_to_true( $value ) {
|
||||
$array = array( 'foo' => $value );
|
||||
|
||||
$this->assertTrue( ArrayUtil::is_truthy( $array, 'foo' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `get_value_or_default` returns the correct value for an existing key.
|
||||
*/
|
||||
public function test_get_value_or_default_returns_value_if_key_exists() {
|
||||
$array = array( 'foo' => 'bar' );
|
||||
|
||||
$this->assertEquals( 'bar', ArrayUtil::get_value_or_default( $array, 'foo' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `get_value_or_default` returns null if the key does not exist and no default value is supplied.
|
||||
*/
|
||||
public function test_get_value_or_default_returns_null_if_key_not_exists_and_no_default_supplied() {
|
||||
$array = array( 'foo' => 'bar' );
|
||||
|
||||
$this->assertNull( ArrayUtil::get_value_or_default( $array, 'fizz' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox `get_value_or_default` returns the supplied default value if the key does not exist.
|
||||
*/
|
||||
public function test_get_value_or_default_returns_supplied_default_value_if_key_not_exists() {
|
||||
$array = array( 'foo' => 'bar' );
|
||||
|
||||
$this->assertEquals( 'buzz', ArrayUtil::get_value_or_default( $array, 'fizz', 'buzz' ) );
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue