PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.6.0-beta.2
WooCommerce v10.6.0-beta.2
10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / ProductAttributesLookup / LookupDataStore.php
woocommerce / src / Internal / ProductAttributesLookup Last commit date
CLIRunner.php 1 year ago DataRegenerator.php 3 months ago Filterer.php 1 year ago LookupDataStore.php 3 months ago
LookupDataStore.php
1089 lines
1 <?php
2 /**
3 * LookupDataStore class file.
4 */
5
6 namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
7
8 use Automattic\WooCommerce\Enums\ProductStockStatus;
9 use Automattic\WooCommerce\Enums\ProductType;
10 use Automattic\WooCommerce\Enums\CatalogVisibility;
11 use Automattic\WooCommerce\Utilities\ArrayUtil;
12 use Automattic\WooCommerce\Utilities\StringUtil;
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Data store class for the product attributes lookup table.
18 */
19 class LookupDataStore {
20
21 /**
22 * Types of updates to perform depending on the current changest
23 */
24
25 public const ACTION_NONE = 0;
26 public const ACTION_INSERT = 1;
27 public const ACTION_UPDATE_STOCK = 2;
28 public const ACTION_DELETE = 3;
29
30 /**
31 * The lookup table name.
32 *
33 * @var string
34 */
35 private $lookup_table_name;
36
37 /**
38 * True if the optimized database access setting is enabled AND products are stored as custom post types.
39 *
40 * @var bool
41 */
42 private bool $optimized_db_access_is_enabled;
43
44 /**
45 * Flag indicating if the last lookup table creation operation failed.
46 *
47 * @var bool
48 */
49 private bool $last_create_operation_failed = false;
50
51 /**
52 * LookupDataStore constructor.
53 */
54 public function __construct() {
55 global $wpdb;
56
57 $this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
58 $this->optimized_db_access_is_enabled =
59 $this->can_use_optimized_db_access() &&
60 'yes' === get_option( 'woocommerce_attribute_lookup_optimized_updates' );
61
62 $this->init_hooks();
63 }
64
65 /**
66 * Initialize the hooks used by the class.
67 */
68 private function init_hooks() {
69 add_action( 'woocommerce_run_product_attribute_lookup_update_callback', array( $this, 'run_update_callback' ), 10, 2 );
70 add_filter( 'woocommerce_get_sections_products', array( $this, 'add_advanced_section_to_product_settings' ), 100, 1 );
71 add_action( 'woocommerce_rest_insert_product', array( $this, 'on_product_created_or_updated_via_rest_api' ), 100, 2 );
72 add_filter( 'woocommerce_get_settings_products', array( $this, 'add_product_attributes_lookup_table_settings' ), 100, 2 );
73 }
74
75 /**
76 * Check if optimized database access can be used when creating lookup table entries.
77 *
78 * @return bool True if optimized database access can be used.
79 */
80 public function can_use_optimized_db_access() {
81 try {
82 return is_a( \WC_Data_Store::load( 'product' )->get_current_class_name(), 'WC_Product_Data_Store_CPT', true );
83 } catch ( \Exception $ex ) {
84 return false;
85 }
86 }
87
88 /**
89 * Check if the lookup table exists in the database.
90 *
91 * @return bool
92 */
93 public function check_lookup_table_exists() {
94 global $wpdb;
95
96 $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->lookup_table_name ) );
97
98 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
99 return $this->lookup_table_name === $wpdb->get_var( $query );
100 }
101
102 /**
103 * Get the name of the lookup table.
104 *
105 * @return string
106 */
107 public function get_lookup_table_name() {
108 return $this->lookup_table_name;
109 }
110
111 /**
112 * Check if the last lookup data creation operation failed.
113 *
114 * @return bool True if the last lookup data creation operation failed.
115 */
116 public function get_last_create_operation_failed() {
117 return $this->last_create_operation_failed;
118 }
119
120 /**
121 * Insert/update the appropriate lookup table entries for a new or modified product or variation.
122 * This must be invoked after a product or a variation is created (including untrashing and duplication)
123 * or modified.
124 *
125 * @param int|\WC_Product $product Product object or product id.
126 * @param null|array $changeset Changes as provided by 'get_changes' method in the product object, null if it's being created.
127 */
128 public function on_product_changed( $product, $changeset = null ) {
129 if ( ! $this->check_lookup_table_exists() ) {
130 return;
131 }
132
133 if ( ! is_a( $product, \WC_Product::class ) ) {
134 $product = WC()->call_function( 'wc_get_product', $product );
135 }
136
137 $action = $this->get_update_action( $changeset );
138 if ( self::ACTION_NONE !== $action ) {
139 $this->maybe_schedule_update( $product->get_id(), $action );
140 }
141 }
142
143 /**
144 * Schedule an update of the product attributes lookup table for a given product.
145 * If an update for the same action is already scheduled, nothing is done.
146 *
147 * If the 'woocommerce_attribute_lookup_direct_update' option is set to 'yes',
148 * the update is done directly, without scheduling.
149 *
150 * @param int $product_id The product id to schedule the update for.
151 * @param int $action The action to perform, one of the ACTION_ constants.
152 */
153 private function maybe_schedule_update( int $product_id, int $action ) {
154 if ( get_option( 'woocommerce_attribute_lookup_direct_updates' ) === 'yes' ) {
155 $this->run_update_callback( $product_id, $action );
156 return;
157 }
158
159 $args = array( $product_id, $action );
160
161 $queue = WC()->get_instance_of( \WC_Queue::class );
162 $already_scheduled = $queue->search(
163 array(
164 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
165 'args' => $args,
166 'status' => \ActionScheduler_Store::STATUS_PENDING,
167 ),
168 'ids'
169 );
170
171 if ( empty( $already_scheduled ) ) {
172 $queue->schedule_single(
173 WC()->call_function( 'time' ) + 1,
174 'woocommerce_run_product_attribute_lookup_update_callback',
175 $args,
176 'woocommerce-db-updates'
177 );
178 }
179 }
180
181 /**
182 * Perform an update of the lookup table for a specific product.
183 *
184 * @param int $product_id The product id to perform the update for.
185 * @param int $action The action to perform, one of the ACTION_ constants.
186 *
187 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
188 */
189 public function run_update_callback( int $product_id, int $action ) {
190 if ( ! $this->check_lookup_table_exists() ) {
191 return;
192 }
193
194 $product = WC()->call_function( 'wc_get_product', $product_id );
195 if ( ! $product ) {
196 $action = self::ACTION_DELETE;
197 }
198
199 switch ( $action ) {
200 case self::ACTION_INSERT:
201 $this->delete_data_for( $product_id );
202 if ( $this->optimized_db_access_is_enabled ) {
203 $this->create_data_for_product_cpt( $product_id );
204 } else {
205 $this->create_data_for( $product );
206 }
207 break;
208 case self::ACTION_UPDATE_STOCK:
209 $this->update_stock_status_for( $product );
210 break;
211 case self::ACTION_DELETE:
212 $this->delete_data_for( $product_id );
213 break;
214 }
215 }
216
217 /**
218 * Determine the type of action to perform depending on the received changeset.
219 *
220 * @param array|null $changeset The changeset received by on_product_changed.
221 * @return int One of the ACTION_ constants.
222 */
223 private function get_update_action( $changeset ) {
224 if ( is_null( $changeset ) ) {
225 // No changeset at all means that the product is new.
226 return self::ACTION_INSERT;
227 }
228
229 $keys = array_keys( $changeset );
230
231 // Order matters:
232 // - The change with the most precedence is a change in catalog visibility
233 // (which will result in all data being regenerated or deleted).
234 // - Then a change in attributes (all data will be regenerated).
235 // - And finally a change in stock status (existing data will be updated).
236 // Thus these conditions must be checked in that same order.
237
238 if ( in_array( 'catalog_visibility', $keys, true ) ) {
239 $new_visibility = $changeset['catalog_visibility'];
240 if ( CatalogVisibility::VISIBLE === $new_visibility || CatalogVisibility::CATALOG === $new_visibility ) {
241 return self::ACTION_INSERT;
242 } else {
243 return self::ACTION_DELETE;
244 }
245 }
246
247 if ( in_array( 'attributes', $keys, true ) ) {
248 return self::ACTION_INSERT;
249 }
250
251 if ( array_intersect( $keys, array( 'stock_quantity', 'stock_status', 'manage_stock' ) ) ) {
252 return self::ACTION_UPDATE_STOCK;
253 }
254
255 return self::ACTION_NONE;
256 }
257
258 /**
259 * Update the stock status of the lookup table entries for a given product.
260 *
261 * @param \WC_Product $product The product to update the entries for.
262 */
263 private function update_stock_status_for( \WC_Product $product ) {
264 global $wpdb;
265
266 $in_stock = $product->is_in_stock();
267
268 $wpdb->query(
269 $wpdb->prepare(
270 'UPDATE %i SET in_stock = %d WHERE product_id = %d',
271 $this->lookup_table_name,
272 $in_stock ? 1 : 0,
273 $product->get_id()
274 )
275 );
276 }
277
278 /**
279 * Delete the lookup table contents related to a given product or variation,
280 * if it's a variable product it deletes the information for variations too.
281 * This must be invoked after a product or a variation is trashed or deleted.
282 *
283 * @param int|\WC_Product $product Product object or product id.
284 */
285 public function on_product_deleted( $product ) {
286 if ( ! $this->check_lookup_table_exists() ) {
287 return;
288 }
289
290 if ( is_a( $product, \WC_Product::class ) ) {
291 $product_id = $product->get_id();
292 } else {
293 $product_id = $product;
294 }
295
296 $this->maybe_schedule_update( $product_id, self::ACTION_DELETE );
297 }
298
299 /**
300 * Create the lookup data for a given product, if a variable product is passed
301 * the information is created for all of its variations.
302 * This method is intended to be called from the data regenerator.
303 *
304 * @param int|WC_Product $product Product object or id.
305 * @param bool $use_optimized_db_access Use direct database access for data retrieval if possible.
306 */
307 public function create_data_for_product( $product, $use_optimized_db_access = false ) {
308 if ( $use_optimized_db_access ) {
309 $product_id = intval( ( $product instanceof \WC_Product ) ? $product->get_id() : $product );
310 $this->create_data_for_product_cpt( $product_id );
311 } else {
312 if ( ! is_a( $product, \WC_Product::class ) ) {
313 $product = WC()->call_function( 'wc_get_product', $product );
314 }
315
316 $this->delete_data_for( $product->get_id() );
317 $this->create_data_for( $product );
318 }
319 }
320
321 /**
322 * Create lookup table data for a given product.
323 *
324 * @param \WC_Product $product The product to create the data for.
325 */
326 private function create_data_for( \WC_Product $product ) {
327 $this->last_create_operation_failed = false;
328
329 try {
330 if ( $this->is_variation( $product ) ) {
331 $this->create_data_for_variation( $product );
332 } elseif ( $this->is_variable_product( $product ) ) {
333 $this->create_data_for_variable_product( $product );
334 } else {
335 $this->create_data_for_simple_product( $product );
336 }
337 } catch ( \Exception $e ) {
338 $product_id = $product->get_id();
339 WC()->call_function( 'wc_get_logger' )->error(
340 "Lookup data creation (not optimized) failed for product $product_id: " . $e->getMessage(),
341 array(
342 'source' => 'palt-updates',
343 'exception' => $e,
344 'product_id' => $product_id,
345 )
346 );
347
348 $this->last_create_operation_failed = true;
349 }
350 }
351
352 /**
353 * Delete all the lookup table entries for a given product,
354 * if it's a variable product information for variations is deleted too.
355 *
356 * @param int $product_id Simple product id, or main/parent product id for variable products.
357 */
358 private function delete_data_for( int $product_id ) {
359 global $wpdb;
360
361 // Single query handled with `index_merge` strategy, while separate with `range` (better performing) on available indexes.
362 $wpdb->query(
363 $wpdb->prepare(
364 'DELETE FROM %i WHERE product_or_parent_id = %d',
365 $this->lookup_table_name,
366 $product_id
367 )
368 );
369 $wpdb->query(
370 $wpdb->prepare(
371 'DELETE FROM %i WHERE product_id = %d',
372 $this->lookup_table_name,
373 $product_id
374 )
375 );
376 }
377
378 /**
379 * Create lookup table entries for a simple (non variable) product.
380 * Assumes that no entries exist yet.
381 *
382 * @param \WC_Product $product The product to create the entries for.
383 */
384 private function create_data_for_simple_product( \WC_Product $product ) {
385 $product_attributes_data = $this->get_attribute_taxonomies( $product );
386 $has_stock = $product->is_in_stock();
387 $product_id = $product->get_id();
388 foreach ( $product_attributes_data as $taxonomy => $data ) {
389 $term_ids = $data['term_ids'];
390 foreach ( $term_ids as $term_id ) {
391 $this->insert_lookup_table_data( $product_id, $product_id, $taxonomy, $term_id, false, $has_stock );
392 }
393 }
394 }
395
396 /**
397 * Create lookup table entries for a variable product.
398 * Assumes that no entries exist yet.
399 *
400 * @param \WC_Product_Variable $product The product to create the entries for.
401 */
402 private function create_data_for_variable_product( \WC_Product_Variable $product ) {
403 $product_attributes_data = $this->get_attribute_taxonomies( $product );
404 $variation_attributes_data = array_filter(
405 $product_attributes_data,
406 function ( $item ) {
407 return $item['used_for_variations'];
408 }
409 );
410 $non_variation_attributes_data = array_filter(
411 $product_attributes_data,
412 function ( $item ) {
413 return ! $item['used_for_variations'];
414 }
415 );
416
417 $main_product_has_stock = $product->is_in_stock();
418 $main_product_id = $product->get_id();
419
420 foreach ( $non_variation_attributes_data as $taxonomy => $data ) {
421 $term_ids = $data['term_ids'];
422 foreach ( $term_ids as $term_id ) {
423 $this->insert_lookup_table_data( $main_product_id, $main_product_id, $taxonomy, $term_id, false, $main_product_has_stock );
424 }
425 }
426
427 $term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );
428 $variations = $this->get_variations_of( $product );
429
430 foreach ( $variation_attributes_data as $taxonomy => $data ) {
431 foreach ( $variations as $variation ) {
432 $this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product_id, $data['term_ids'], $term_ids_by_slug_cache );
433 }
434 }
435 }
436
437 /**
438 * Create all the necessary lookup data for a given variation.
439 *
440 * @param \WC_Product_Variation $variation The variation to create entries for.
441 * @throws \Exception Can't retrieve the details of the parent product.
442 */
443 private function create_data_for_variation( \WC_Product_Variation $variation ) {
444 $main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() );
445 if ( false === $main_product ) {
446 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
447 throw new \Exception( "The product is a variation, and the retrieval of data for the parent product (id {$variation->get_parent_id()}) failed." );
448 }
449
450 $product_attributes_data = $this->get_attribute_taxonomies( $main_product );
451 $variation_attributes_data = array_filter(
452 $product_attributes_data,
453 function ( $item ) {
454 return $item['used_for_variations'];
455 }
456 );
457
458 $term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );
459
460 foreach ( $variation_attributes_data as $taxonomy => $data ) {
461 $this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product->get_id(), $data['term_ids'], $term_ids_by_slug_cache );
462 }
463 }
464
465 /**
466 * Create lookup table entries for a given variation, corresponding to a given taxonomy and a set of term ids.
467 *
468 * @param \WC_Product_Variation $variation The variation to create entries for.
469 * @param string $taxonomy The taxonomy to create the entries for.
470 * @param int $main_product_id The parent product id.
471 * @param array $term_ids The term ids to create entries for.
472 * @param array $term_ids_by_slug_cache A dictionary of term ids by term slug, as returned by 'get_term_ids_by_slug_cache'.
473 */
474 private function insert_lookup_table_data_for_variation( \WC_Product_Variation $variation, string $taxonomy, int $main_product_id, array $term_ids, array $term_ids_by_slug_cache ) {
475 $variation_id = $variation->get_id();
476 $variation_has_stock = $variation->is_in_stock();
477 $variation_definition_term_id = $this->get_variation_definition_term_id( $variation, $taxonomy, $term_ids_by_slug_cache );
478 if ( $variation_definition_term_id ) {
479 $this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $variation_definition_term_id, true, $variation_has_stock );
480 } else {
481 $term_ids_for_taxonomy = $term_ids;
482 foreach ( $term_ids_for_taxonomy as $term_id ) {
483 $this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $term_id, true, $variation_has_stock );
484 }
485 }
486 }
487
488 /**
489 * Get a cache of term ids by slug for a set of taxonomies, with this format:
490 *
491 * [
492 * 'taxonomy' => [
493 * 'slug_1' => id_1,
494 * 'slug_2' => id_2,
495 * ...
496 * ], ...
497 * ]
498 *
499 * @param array $taxonomies List of taxonomies to build the cache for.
500 * @return array A dictionary of taxonomies => dictionary of term slug => term id.
501 */
502 private function get_term_ids_by_slug_cache( $taxonomies ) {
503 $result = array();
504 foreach ( $taxonomies as $taxonomy ) {
505 $terms = WC()->call_function(
506 'get_terms',
507 array(
508 'taxonomy' => wc_sanitize_taxonomy_name( $taxonomy ),
509 'hide_empty' => false,
510 'fields' => 'id=>slug',
511 )
512 );
513 $result[ $taxonomy ] = array_flip( $terms );
514 }
515 return $result;
516 }
517
518 /**
519 * Get the id of the term that defines a variation for a given taxonomy,
520 * or null if there's no such defining id (for variations having "Any <taxonomy>" as the definition)
521 *
522 * @param \WC_Product_Variation $variation The variation to get the defining term id for.
523 * @param string $taxonomy The taxonomy to get the defining term id for.
524 * @param array $term_ids_by_slug_cache A term ids by slug as generated by get_term_ids_by_slug_cache.
525 * @return int|null The term id, or null if there's no defining id for that taxonomy in that variation.
526 */
527 private function get_variation_definition_term_id( \WC_Product_Variation $variation, string $taxonomy, array $term_ids_by_slug_cache ) {
528 $variation_attributes = $variation->get_attributes();
529 $term_slug = ArrayUtil::get_value_or_default( $variation_attributes, $taxonomy );
530 if ( $term_slug ) {
531 return $term_ids_by_slug_cache[ $taxonomy ][ $term_slug ];
532 } else {
533 return null;
534 }
535 }
536
537 /**
538 * Get the variations of a given variable product.
539 *
540 * @param \WC_Product_Variable $product The product to get the variations for.
541 * @return array An array of WC_Product_Variation objects.
542 */
543 private function get_variations_of( \WC_Product_Variable $product ) {
544 $variation_ids = $product->get_children();
545 return array_map(
546 function ( $id ) {
547 return WC()->call_function( 'wc_get_product', $id );
548 },
549 $variation_ids
550 );
551 }
552
553 /**
554 * Check if a given product is a variable product.
555 *
556 * @param \WC_Product $product The product to check.
557 * @return bool True if it's a variable product, false otherwise.
558 */
559 private function is_variable_product( \WC_Product $product ) {
560 return is_a( $product, \WC_Product_Variable::class );
561 }
562
563 /**
564 * Check if a given product is a variation.
565 *
566 * @param \WC_Product $product The product to check.
567 * @return bool True if it's a variation, false otherwise.
568 */
569 private function is_variation( \WC_Product $product ) {
570 return is_a( $product, \WC_Product_Variation::class );
571 }
572
573 /**
574 * Return the list of taxonomies used for variations on a product together with
575 * the associated term ids, with the following format:
576 *
577 * [
578 * 'taxonomy_name' =>
579 * [
580 * 'term_ids' => [id, id, ...],
581 * 'used_for_variations' => true|false
582 * ], ...
583 * ]
584 *
585 * @param \WC_Product $product The product to get the attribute taxonomies for.
586 * @return array Information about the attribute taxonomies of the product.
587 */
588 private function get_attribute_taxonomies( \WC_Product $product ) {
589 $product_attributes = $product->get_attributes();
590 $result = array();
591 foreach ( $product_attributes as $taxonomy_name => $attribute_data ) {
592 if ( ! $attribute_data->get_id() ) {
593 // Custom product attribute, not suitable for attribute-based filtering.
594 continue;
595 }
596
597 $result[ $taxonomy_name ] = array(
598 'term_ids' => $attribute_data->get_options(),
599 'used_for_variations' => $attribute_data->get_variation(),
600 );
601 }
602
603 return $result;
604 }
605
606 /**
607 * Insert one entry in the lookup table.
608 *
609 * @param int $product_id The product id.
610 * @param int $product_or_parent_id The product id for non-variable products, the main/parent product id for variations.
611 * @param string $taxonomy Taxonomy name.
612 * @param int $term_id Term id.
613 * @param bool $is_variation_attribute True if the taxonomy corresponds to an attribute used to define variations.
614 * @param bool $has_stock True if the product is in stock.
615 */
616 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 ) {
617 global $wpdb;
618
619 $wpdb->query(
620 $wpdb->prepare(
621 'INSERT INTO %i (
622 product_id,
623 product_or_parent_id,
624 taxonomy,
625 term_id,
626 is_variation_attribute,
627 in_stock)
628 VALUES
629 ( %d, %d, %s, %d, %d, %d )',
630 $this->lookup_table_name,
631 $product_id,
632 $product_or_parent_id,
633 $taxonomy,
634 $term_id,
635 $is_variation_attribute ? 1 : 0,
636 $has_stock ? 1 : 0
637 )
638 );
639 }
640
641 /**
642 * Handler for the woocommerce_rest_insert_product hook.
643 * Needed to update the lookup table when the REST API batch insert/update endpoints are used.
644 *
645 * @param \WP_Post $product The post representing the created or updated product.
646 * @param \WP_REST_Request $request The REST request that caused the hook to be fired.
647 * @return void
648 *
649 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
650 */
651 public function on_product_created_or_updated_via_rest_api( \WP_Post $product, \WP_REST_Request $request ): void {
652 if ( StringUtil::ends_with( $request->get_route(), '/batch' ) ) {
653 $this->on_product_changed( $product->ID );
654 }
655 }
656
657 /**
658 * Tells if a lookup table regeneration is currently in progress.
659 *
660 * @return bool True if a lookup table regeneration is already in progress.
661 */
662 public function regeneration_is_in_progress() {
663 return get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null ) === 'yes';
664 }
665
666 /**
667 * Set a permanent flag (via option) indicating that the lookup table regeneration is in process.
668 */
669 public function set_regeneration_in_progress_flag() {
670 update_option( 'woocommerce_attribute_lookup_regeneration_in_progress', 'yes' );
671 }
672
673 /**
674 * Remove the flag indicating that the lookup table regeneration is in process.
675 */
676 public function unset_regeneration_in_progress_flag() {
677 delete_option( 'woocommerce_attribute_lookup_regeneration_in_progress' );
678 }
679
680 /**
681 * Set a flag indicating that the last lookup table regeneration process started was aborted.
682 */
683 public function set_regeneration_aborted_flag() {
684 update_option( 'woocommerce_attribute_lookup_regeneration_aborted', 'yes' );
685 }
686
687 /**
688 * Remove the flag indicating that the last lookup table regeneration process started was aborted.
689 */
690 public function unset_regeneration_aborted_flag() {
691 delete_option( 'woocommerce_attribute_lookup_regeneration_aborted' );
692 }
693
694 /**
695 * Tells if the last lookup table regeneration process started was aborted
696 * (via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option).
697 *
698 * @return bool True if the last lookup table regeneration process was aborted.
699 */
700 public function regeneration_was_aborted(): bool {
701 return get_option( 'woocommerce_attribute_lookup_regeneration_aborted' ) === 'yes';
702 }
703
704 /**
705 * Check if the lookup table contains any entry at all.
706 *
707 * @return bool True if the table contains entries, false if the table is empty.
708 */
709 public function lookup_table_has_data(): bool {
710 global $wpdb;
711
712 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
713 return ( (int) $wpdb->get_var( "SELECT EXISTS (SELECT 1 FROM {$this->lookup_table_name})" ) ) !== 0;
714 }
715
716 /**
717 * Handler for 'woocommerce_get_sections_products', adds the "Advanced" section to the product settings.
718 *
719 * @param array $products Original array of settings sections.
720 * @return array New array of settings sections.
721 *
722 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
723 */
724 public function add_advanced_section_to_product_settings( array $products ): array {
725 if ( $this->check_lookup_table_exists() ) {
726 $products['advanced'] = __( 'Advanced', 'woocommerce' );
727 }
728
729 return $products;
730 }
731
732 /**
733 * Handler for 'woocommerce_get_settings_products', adds the settings related to the product attributes lookup table.
734 *
735 * @param array $settings Original settings configuration array.
736 * @param string $section_id Settings section identifier.
737 * @return array New settings configuration array.
738 *
739 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
740 */
741 public function add_product_attributes_lookup_table_settings( array $settings, string $section_id ): array {
742 if ( 'advanced' === $section_id && $this->check_lookup_table_exists() ) {
743 $title_item = array(
744 'title' => __( 'Product attributes lookup table', 'woocommerce' ),
745 'type' => 'title',
746 );
747
748 $regeneration_is_in_progress = $this->regeneration_is_in_progress();
749
750 if ( $regeneration_is_in_progress ) {
751 $title_item['desc'] = __( 'These settings are not available while the lookup table regeneration is in progress.', 'woocommerce' );
752 }
753
754 $settings[] = $title_item;
755
756 if ( ! $regeneration_is_in_progress ) {
757 $regeneration_aborted_warning =
758 $this->regeneration_was_aborted() ?
759 sprintf(
760 "<p><strong style='color: #E00000'>%s</strong></p><p>%s</p>",
761 __( 'WARNING: The product attributes lookup table regeneration process was aborted.', 'woocommerce' ),
762 __( 'This means that the table is probably in an inconsistent state. It\'s recommended to run a new regeneration process or to resume the aborted process (Status - Tools - Regenerate the product attributes lookup table/Resume the product attributes lookup table regeneration) before enabling the table usage.', 'woocommerce' )
763 ) : null;
764
765 $settings[] = array(
766 'title' => __( 'Enable table usage', 'woocommerce' ),
767 'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ),
768 'desc_tip' => $regeneration_aborted_warning,
769 'id' => 'woocommerce_attribute_lookup_enabled',
770 'default' => 'no',
771 'type' => 'checkbox',
772 'checkboxgroup' => 'start',
773 );
774
775 $settings[] = array(
776 'title' => __( 'Direct updates', 'woocommerce' ),
777 'desc' => __( 'Update the table directly upon product changes, instead of scheduling a deferred update.', 'woocommerce' ),
778 'id' => 'woocommerce_attribute_lookup_direct_updates',
779 'default' => 'no',
780 'type' => 'checkbox',
781 'checkboxgroup' => 'start',
782 );
783
784 $settings[] = array(
785 'title' => __( 'Optimized updates', 'woocommerce' ),
786 'desc' => __( 'Uses much more performant queries to update the lookup table, but may not be compatible with some extensions.', 'woocommerce' ),
787 'desc_tip' => __( 'This setting only works when product data is stored in the posts table.', 'woocommerce' ),
788 'id' => 'woocommerce_attribute_lookup_optimized_updates',
789 'default' => 'no',
790 'type' => 'checkbox',
791 'checkboxgroup' => 'start',
792 );
793 }
794
795 $settings[] = array( 'type' => 'sectionend' );
796 }
797
798 return $settings;
799 }
800
801 /**
802 * Check if the optimized database access setting is enabled.
803 *
804 * @return bool True if the optimized database access setting is enabled.
805 */
806 public function optimized_data_access_is_enabled() {
807 return 'yes' === get_option( 'woocommerce_attribute_lookup_optimized_updates' );
808 }
809
810 /**
811 * Create the lookup table data for a product or variation using optimized database access.
812 * For variable products entries are created for the main product and for all the variations.
813 *
814 * @param int $product_id Product or variation id.
815 */
816 private function create_data_for_product_cpt( int $product_id ) {
817 $this->last_create_operation_failed = false;
818
819 try {
820 $this->create_data_for_product_cpt_core( $product_id );
821 } catch ( \Exception $e ) {
822 $data = array(
823 'source' => 'palt-updates',
824 'product_id' => $product_id,
825 );
826
827 if ( $e instanceof \WC_Data_Exception ) {
828 $data = array_merge( $data, $e->getErrorData() );
829 } else {
830 $data['exception'] = $e;
831 }
832
833 WC()->call_function( 'wc_get_logger' )
834 ->error( "Lookup data creation (optimized) failed for product $product_id: " . $e->getMessage(), $data );
835
836 $this->last_create_operation_failed = true;
837 }
838 }
839
840 /**
841 * Core version of create_data_for_product_cpt (doesn't catch exceptions).
842 *
843 * @param int $product_id Product or variation id.
844 * @return void
845 * @throws \WC_Data_Exception Wrongly serialized attribute data found, or INSERT statement failed.
846 */
847 private function create_data_for_product_cpt_core( int $product_id ) {
848 global $wpdb;
849
850 $wpdb->query(
851 $wpdb->prepare(
852 'DELETE FROM %i WHERE product_or_parent_id = %d',
853 $this->lookup_table_name,
854 $product_id
855 )
856 );
857
858 // * Obtain list of product variations, together with stock statuses; also get the product type.
859 // For a variation this will return just one entry, with type 'variation'.
860 // Output: $product_ids_with_stock_status = associative array where 'id' is the key and values are the stock status (1 for "in stock", 0 otherwise).
861 // $variation_ids = raw list of variation ids.
862 // $is_variable_product = true or false.
863 // $is_variation = true or false.
864
865 $sql = $wpdb->prepare(
866 "(select p.ID as id, null parent, m.meta_value as stock_status, t.name as product_type from {$wpdb->posts} p
867 left join {$wpdb->postmeta} m on p.id=m.post_id and m.meta_key='_stock_status'
868 left join {$wpdb->term_relationships} tr on tr.object_id=p.id
869 left join {$wpdb->term_taxonomy} tt on tt.term_taxonomy_id=tr.term_taxonomy_id
870 left join {$wpdb->terms} t on t.term_id=tt.term_id
871 where p.post_type = 'product'
872 and p.post_status in ('publish', 'draft', 'pending', 'private')
873 and tt.taxonomy='product_type'
874 and t.name != 'exclude-from-search'
875 and p.id=%d
876 limit 1)
877 union
878 (select p.ID as id, p.post_parent as parent, m.meta_value as stock_status, 'variation' as product_type from {$wpdb->posts} p
879 left join {$wpdb->postmeta} m on p.id=m.post_id and m.meta_key='_stock_status'
880 where p.post_type = 'product_variation'
881 and p.post_status in ('publish', 'draft', 'pending', 'private')
882 and (p.ID=%d or p.post_parent=%d));
883 ",
884 $product_id,
885 $product_id,
886 $product_id
887 );
888
889 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
890 $product_ids_with_stock_status = $wpdb->get_results( $sql, ARRAY_A );
891
892 $main_product_row = array_filter( $product_ids_with_stock_status, fn( $item ) => ProductType::VARIATION !== $item['product_type'] );
893 $is_variation = empty( $main_product_row );
894
895 $main_product_id =
896 $is_variation ?
897 current( $product_ids_with_stock_status )['parent'] :
898 $product_id;
899
900 $is_variable_product = ! $is_variation && ( ProductType::VARIABLE === current( $main_product_row )['product_type'] );
901
902 $product_ids_with_stock_status = ArrayUtil::group_by_column( $product_ids_with_stock_status, 'id', true );
903 $variation_ids = $is_variation ? array( $product_id ) : array_keys( array_diff_key( $product_ids_with_stock_status, array( $product_id => null ) ) );
904 $product_ids_with_stock_status = ArrayUtil::select( $product_ids_with_stock_status, 'stock_status' );
905
906 $product_ids_with_stock_status = array_map( fn( $item ) => ProductStockStatus::IN_STOCK === $item ? 1 : 0, $product_ids_with_stock_status );
907
908 // * Obtain the list of attributes used for variations and not.
909 // Output: two lists of attribute slugs, all starting with 'pa_'.
910
911 $sql = $wpdb->prepare(
912 "select meta_value from {$wpdb->postmeta} where post_id=%d and meta_key=%s",
913 $main_product_id,
914 '_product_attributes'
915 );
916
917 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
918 $temp = $wpdb->get_var( $sql );
919
920 if ( is_null( $temp ) ) {
921 // The product has no attributes, thus there's no attributes lookup data to generate.
922 return;
923 }
924
925 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
926 $temp = unserialize( $temp );
927 if ( false === $temp ) {
928 throw new \WC_Data_Exception( 0, 'The product attributes metadata row is not properly serialized' );
929 }
930
931 $temp = array_filter( $temp, fn( $item, $slug ) => StringUtil::starts_with( $slug, 'pa_' ) && '' === $item['value'], ARRAY_FILTER_USE_BOTH );
932
933 $attributes_not_for_variations =
934 $is_variation || $is_variable_product ?
935 array_keys( array_filter( $temp, fn( $item ) => 0 === $item['is_variation'] ) ) :
936 array_keys( $temp );
937
938 // * Obtain the terms used for each attribute.
939 // Output: $terms_used_per_attribute =
940 // [
941 // 'pa_...' => [
942 // [
943 // 'term_id' => <term id>,
944 // 'attribute' => 'pa_...'
945 // 'slug' => <term slug>
946 // ],...
947 // ],...
948 // ]
949
950 $sql = $wpdb->prepare(
951 "select tt.term_id, tt.taxonomy as attribute, t.slug from {$wpdb->prefix}term_relationships tr
952 join {$wpdb->term_taxonomy} tt on tt.term_taxonomy_id = tr.term_taxonomy_id
953 join {$wpdb->terms} t on t.term_id=tt.term_id
954 where tr.object_id=%d and taxonomy like %s;",
955 $main_product_id,
956 'pa_%'
957 );
958
959 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
960 $terms_used_per_attribute = $wpdb->get_results( $sql, ARRAY_A );
961 foreach ( $terms_used_per_attribute as &$term ) {
962 $term['attribute'] = strtolower( rawurlencode( $term['attribute'] ) );
963 }
964 $terms_used_per_attribute = ArrayUtil::group_by_column( $terms_used_per_attribute, 'attribute' );
965
966 // * Obtain the actual variations defined (only if variations exist).
967 // Output: $variations_defined =
968 // [
969 // <variation id> => [
970 // [
971 // 'variation_id' => <variation id>,
972 // 'attribute' => 'pa_...'
973 // 'slug' => <term slug>
974 // ],...
975 // ],...
976 // ]
977 //
978 // Note that this does NOT include "any..." attributes!
979
980 if ( ! $is_variation && ( ! $is_variable_product || empty( $variation_ids ) ) ) {
981 $variations_defined = array();
982 } else {
983 $sql = $wpdb->prepare(
984 "select post_id as variation_id, substr(meta_key,11) as attribute, meta_value as slug from {$wpdb->postmeta}
985 where post_id in (select ID from {$wpdb->posts} where (id=%d or post_parent=%d) and post_type = 'product_variation')
986 and meta_key like %s
987 and meta_value != ''",
988 $product_id,
989 $product_id,
990 'attribute_pa_%'
991 );
992 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
993 $variations_defined = $wpdb->get_results( $sql, ARRAY_A );
994 $variations_defined = ArrayUtil::group_by_column( $variations_defined, 'variation_id' );
995 }
996
997 // Now we'll fill an array with all the data rows to be inserted in the lookup table.
998
999 $insert_data = array();
1000
1001 // * Insert data for the main product
1002
1003 if ( ! $is_variation ) {
1004 foreach ( $attributes_not_for_variations as $attribute_name ) {
1005 foreach ( ( $terms_used_per_attribute[ $attribute_name ] ?? array() ) as $attribute_data ) {
1006 $insert_data[] = array( $product_id, $main_product_id, $attribute_name, $attribute_data['term_id'], 0, $product_ids_with_stock_status[ $product_id ] );
1007 }
1008 }
1009 }
1010
1011 // * Insert data for the variations defined
1012
1013 // Remove the non-variation attributes data first.
1014 $terms_used_per_attribute = array_diff_key( $terms_used_per_attribute, array_flip( $attributes_not_for_variations ) );
1015
1016 $used_attributes_per_variation = array();
1017 foreach ( $variations_defined as $variation_id => $variation_data ) {
1018 $used_attributes_per_variation[ $variation_id ] = array();
1019 foreach ( $variation_data as $variation_attribute_data ) {
1020 $attribute_name = $variation_attribute_data['attribute'];
1021 $used_attributes_per_variation[ $variation_id ][] = $attribute_name;
1022 $term_id = current( array_filter( ( $terms_used_per_attribute[ $attribute_name ] ?? array() ), fn( $item ) => $item['slug'] === $variation_attribute_data['slug'] ) )['term_id'] ?? null;
1023 if ( is_null( $term_id ) ) {
1024 continue;
1025 }
1026 $insert_data[] = array( $variation_id, $main_product_id, $attribute_name, $term_id, 1, $product_ids_with_stock_status[ $variation_id ] ?? false );
1027 }
1028 }
1029
1030 // * Insert data for variations that have "any..." attributes and at least one defined attribute
1031
1032 foreach ( $used_attributes_per_variation as $variation_id => $attributes_list ) {
1033 $any_attributes = array_diff_key( $terms_used_per_attribute, array_flip( $attributes_list ) );
1034 foreach ( $any_attributes as $attributes_data ) {
1035 foreach ( $attributes_data as $attribute_data ) {
1036 $insert_data[] = array( $variation_id, $main_product_id, $attribute_data['attribute'], $attribute_data['term_id'], 1, $product_ids_with_stock_status[ $variation_id ] ?? false );
1037 }
1038 }
1039 }
1040
1041 // * Insert data for variations that have all their attributes defined as "any..."
1042
1043 $variations_with_all_any = array_keys( array_diff_key( array_flip( $variation_ids ), $used_attributes_per_variation ) );
1044 foreach ( $variations_with_all_any as $variation_id ) {
1045 foreach ( $terms_used_per_attribute as $attribute_name => $attribute_terms ) {
1046 foreach ( $attribute_terms as $attribute_term ) {
1047 $insert_data[] = array( $variation_id, $main_product_id, $attribute_name, $attribute_term['term_id'], 1, $product_ids_with_stock_status[ $variation_id ] ?? false );
1048 }
1049 }
1050 }
1051
1052 // * We have all the data to insert, let's go and insert it.
1053
1054 $insert_data_chunks = array_chunk( $insert_data, 100 );
1055 foreach ( $insert_data_chunks as $insert_data_chunk ) {
1056 $sql = 'INSERT INTO ' . $this->lookup_table_name . ' (
1057 product_id,
1058 product_or_parent_id,
1059 taxonomy,
1060 term_id,
1061 is_variation_attribute,
1062 in_stock)
1063 VALUES (';
1064
1065 $values_strings = array();
1066 foreach ( $insert_data_chunk as $dataset ) {
1067 $attribute_name = esc_sql( $dataset[2] );
1068 $values_strings[] = "{$dataset[0]},{$dataset[1]},'{$attribute_name}',{$dataset[3]},{$dataset[4]},{$dataset[5]}";
1069 }
1070
1071 $sql .= implode( '),(', $values_strings ) . ')';
1072
1073 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1074 $result = $wpdb->query( $sql );
1075 if ( false === $result ) {
1076 throw new \WC_Data_Exception(
1077 0,
1078 'INSERT statement failed',
1079 0,
1080 array(
1081 'db_error' => esc_html( $wpdb->last_error ),
1082 'db_query' => esc_html( $wpdb->last_query ),
1083 )
1084 );
1085 }
1086 }
1087 }
1088 }
1089