PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 6.6.0-rc.2
WooCommerce v6.6.0-rc.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
DataRegenerator.php 4 years ago Filterer.php 4 years ago LookupDataStore.php 4 years ago
LookupDataStore.php
719 lines
1 <?php
2 /**
3 * LookupDataStore class file.
4 */
5
6 namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
7
8 use Automattic\WooCommerce\Utilities\ArrayUtil;
9 use Automattic\WooCommerce\Utilities\StringUtil;
10
11 defined( 'ABSPATH' ) || exit;
12
13 /**
14 * Data store class for the product attributes lookup table.
15 */
16 class LookupDataStore {
17
18 /**
19 * Types of updates to perform depending on the current changest
20 */
21
22 public const ACTION_NONE = 0;
23 public const ACTION_INSERT = 1;
24 public const ACTION_UPDATE_STOCK = 2;
25 public const ACTION_DELETE = 3;
26
27 /**
28 * The lookup table name.
29 *
30 * @var string
31 */
32 private $lookup_table_name;
33
34 /**
35 * LookupDataStore constructor. Makes the feature hidden by default.
36 */
37 public function __construct() {
38 global $wpdb;
39
40 $this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
41
42 $this->init_hooks();
43 }
44
45 /**
46 * Initialize the hooks used by the class.
47 */
48 private function init_hooks() {
49 add_action(
50 'woocommerce_run_product_attribute_lookup_update_callback',
51 function ( $product_id, $action ) {
52 $this->run_update_callback( $product_id, $action );
53 },
54 10,
55 2
56 );
57
58 add_filter(
59 'woocommerce_get_sections_products',
60 function ( $products ) {
61 if ( $this->check_lookup_table_exists() ) {
62 $products['advanced'] = __( 'Advanced', 'woocommerce' );
63 }
64 return $products;
65 },
66 100,
67 1
68 );
69
70 add_action(
71 'woocommerce_rest_insert_product',
72 function ( $product_post, $request ) {
73 $this->on_product_created_or_updated_via_rest_api( $product_post, $request );
74 },
75 100,
76 2
77 );
78
79 add_filter(
80 'woocommerce_get_settings_products',
81 function ( $settings, $section_id ) {
82 if ( 'advanced' === $section_id && $this->check_lookup_table_exists() ) {
83 $title_item = array(
84 'title' => __( 'Product attributes lookup table', 'woocommerce' ),
85 'type' => 'title',
86 );
87
88 $regeneration_is_in_progress = $this->regeneration_is_in_progress();
89
90 if ( $regeneration_is_in_progress ) {
91 $title_item['desc'] = __( 'These settings are not available while the lookup table regeneration is in progress.', 'woocommerce' );
92 }
93
94 $settings[] = $title_item;
95
96 if ( ! $regeneration_is_in_progress ) {
97 $regeneration_aborted_warning =
98 $this->regeneration_was_aborted() ?
99 sprintf(
100 "<p><strong style='color: #E00000'>%s</strong></p><p>%s</p>",
101 __( 'WARNING: The product attributes lookup table regeneration process was aborted.', 'woocommerce' ),
102 __( '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' )
103 ) : null;
104
105 $settings[] = array(
106 'title' => __( 'Enable table usage', 'woocommerce' ),
107 'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ),
108 'desc_tip' => $regeneration_aborted_warning,
109 'id' => 'woocommerce_attribute_lookup_enabled',
110 'default' => 'no',
111 'type' => 'checkbox',
112 'checkboxgroup' => 'start',
113 );
114
115 $settings[] = array(
116 'title' => __( 'Direct updates', 'woocommerce' ),
117 'desc' => __( 'Update the table directly upon product changes, instead of scheduling a deferred update.', 'woocommerce' ),
118 'id' => 'woocommerce_attribute_lookup_direct_updates',
119 'default' => 'no',
120 'type' => 'checkbox',
121 'checkboxgroup' => 'start',
122 );
123 }
124
125 $settings[] = array( 'type' => 'sectionend' );
126 }
127 return $settings;
128 },
129 100,
130 2
131 );
132 }
133
134 /**
135 * Check if the lookup table exists in the database.
136 *
137 * @return bool
138 */
139 public function check_lookup_table_exists() {
140 global $wpdb;
141
142 $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->lookup_table_name ) );
143
144 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
145 return $this->lookup_table_name === $wpdb->get_var( $query );
146 }
147
148 /**
149 * Get the name of the lookup table.
150 *
151 * @return string
152 */
153 public function get_lookup_table_name() {
154 return $this->lookup_table_name;
155 }
156
157 /**
158 * Insert/update the appropriate lookup table entries for a new or modified product or variation.
159 * This must be invoked after a product or a variation is created (including untrashing and duplication)
160 * or modified.
161 *
162 * @param int|\WC_Product $product Product object or product id.
163 * @param null|array $changeset Changes as provided by 'get_changes' method in the product object, null if it's being created.
164 */
165 public function on_product_changed( $product, $changeset = null ) {
166 if ( ! $this->check_lookup_table_exists() ) {
167 return;
168 }
169
170 if ( ! is_a( $product, \WC_Product::class ) ) {
171 $product = WC()->call_function( 'wc_get_product', $product );
172 }
173
174 $action = $this->get_update_action( $changeset );
175 if ( self::ACTION_NONE !== $action ) {
176 $this->maybe_schedule_update( $product->get_id(), $action );
177 }
178 }
179
180 /**
181 * Schedule an update of the product attributes lookup table for a given product.
182 * If an update for the same action is already scheduled, nothing is done.
183 *
184 * If the 'woocommerce_attribute_lookup_direct_update' option is set to 'yes',
185 * the update is done directly, without scheduling.
186 *
187 * @param int $product_id The product id to schedule the update for.
188 * @param int $action The action to perform, one of the ACTION_ constants.
189 */
190 private function maybe_schedule_update( int $product_id, int $action ) {
191 if ( 'yes' === get_option( 'woocommerce_attribute_lookup_direct_updates' ) ) {
192 $this->run_update_callback( $product_id, $action );
193 return;
194 }
195
196 $args = array( $product_id, $action );
197
198 $queue = WC()->get_instance_of( \WC_Queue::class );
199 $already_scheduled = $queue->search(
200 array(
201 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
202 'args' => $args,
203 'status' => \ActionScheduler_Store::STATUS_PENDING,
204 ),
205 'ids'
206 );
207
208 if ( empty( $already_scheduled ) ) {
209 $queue->schedule_single(
210 WC()->call_function( 'time' ) + 1,
211 'woocommerce_run_product_attribute_lookup_update_callback',
212 $args,
213 'woocommerce-db-updates'
214 );
215 }
216 }
217
218 /**
219 * Perform an update of the lookup table for a specific product.
220 *
221 * @param int $product_id The product id to perform the update for.
222 * @param int $action The action to perform, one of the ACTION_ constants.
223 */
224 private function run_update_callback( int $product_id, int $action ) {
225 if ( ! $this->check_lookup_table_exists() ) {
226 return;
227 }
228
229 $product = WC()->call_function( 'wc_get_product', $product_id );
230 if ( ! $product ) {
231 $action = self::ACTION_DELETE;
232 }
233
234 switch ( $action ) {
235 case self::ACTION_INSERT:
236 $this->delete_data_for( $product_id );
237 $this->create_data_for( $product );
238 break;
239 case self::ACTION_UPDATE_STOCK:
240 $this->update_stock_status_for( $product );
241 break;
242 case self::ACTION_DELETE:
243 $this->delete_data_for( $product_id );
244 break;
245 }
246 }
247
248 /**
249 * Determine the type of action to perform depending on the received changeset.
250 *
251 * @param array|null $changeset The changeset received by on_product_changed.
252 * @return int One of the ACTION_ constants.
253 */
254 private function get_update_action( $changeset ) {
255 if ( is_null( $changeset ) ) {
256 // No changeset at all means that the product is new.
257 return self::ACTION_INSERT;
258 }
259
260 $keys = array_keys( $changeset );
261
262 // Order matters:
263 // - The change with the most precedence is a change in catalog visibility
264 // (which will result in all data being regenerated or deleted).
265 // - Then a change in attributes (all data will be regenerated).
266 // - And finally a change in stock status (existing data will be updated).
267 // Thus these conditions must be checked in that same order.
268
269 if ( in_array( 'catalog_visibility', $keys, true ) ) {
270 $new_visibility = $changeset['catalog_visibility'];
271 if ( 'visible' === $new_visibility || 'catalog' === $new_visibility ) {
272 return self::ACTION_INSERT;
273 } else {
274 return self::ACTION_DELETE;
275 }
276 }
277
278 if ( in_array( 'attributes', $keys, true ) ) {
279 return self::ACTION_INSERT;
280 }
281
282 if ( array_intersect( $keys, array( 'stock_quantity', 'stock_status', 'manage_stock' ) ) ) {
283 return self::ACTION_UPDATE_STOCK;
284 }
285
286 return self::ACTION_NONE;
287 }
288
289 /**
290 * Update the stock status of the lookup table entries for a given product.
291 *
292 * @param \WC_Product $product The product to update the entries for.
293 */
294 private function update_stock_status_for( \WC_Product $product ) {
295 global $wpdb;
296
297 $in_stock = $product->is_in_stock();
298
299 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
300 $wpdb->query(
301 $wpdb->prepare(
302 'UPDATE ' . $this->lookup_table_name . ' SET in_stock = %d WHERE product_id = %d',
303 $in_stock ? 1 : 0,
304 $product->get_id()
305 )
306 );
307 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
308 }
309
310 /**
311 * Delete the lookup table contents related to a given product or variation,
312 * if it's a variable product it deletes the information for variations too.
313 * This must be invoked after a product or a variation is trashed or deleted.
314 *
315 * @param int|\WC_Product $product Product object or product id.
316 */
317 public function on_product_deleted( $product ) {
318 if ( ! $this->check_lookup_table_exists() ) {
319 return;
320 }
321
322 if ( is_a( $product, \WC_Product::class ) ) {
323 $product_id = $product->get_id();
324 } else {
325 $product_id = $product;
326 }
327
328 $this->maybe_schedule_update( $product_id, self::ACTION_DELETE );
329 }
330
331 /**
332 * Create the lookup data for a given product, if a variable product is passed
333 * the information is created for all of its variations.
334 * This method is intended to be called from the data regenerator.
335 *
336 * @param int|WC_Product $product Product object or id.
337 * @throws \Exception A variation object is passed.
338 */
339 public function create_data_for_product( $product ) {
340 if ( ! is_a( $product, \WC_Product::class ) ) {
341 $product = WC()->call_function( 'wc_get_product', $product );
342 }
343
344 if ( $this->is_variation( $product ) ) {
345 throw new \Exception( "LookupDataStore::create_data_for_product can't be called for variations." );
346 }
347
348 $this->delete_data_for( $product->get_id() );
349 $this->create_data_for( $product );
350 }
351
352 /**
353 * Create lookup table data for a given product.
354 *
355 * @param \WC_Product $product The product to create the data for.
356 */
357 private function create_data_for( \WC_Product $product ) {
358 if ( $this->is_variation( $product ) ) {
359 $this->create_data_for_variation( $product );
360 } elseif ( $this->is_variable_product( $product ) ) {
361 $this->create_data_for_variable_product( $product );
362 } else {
363 $this->create_data_for_simple_product( $product );
364 }
365 }
366
367 /**
368 * Delete all the lookup table entries for a given product,
369 * if it's a variable product information for variations is deleted too.
370 *
371 * @param int $product_id Simple product id, or main/parent product id for variable products.
372 */
373 private function delete_data_for( int $product_id ) {
374 global $wpdb;
375
376 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
377 $wpdb->query(
378 $wpdb->prepare(
379 'DELETE FROM ' . $this->lookup_table_name . ' WHERE product_id = %d OR product_or_parent_id = %d',
380 $product_id,
381 $product_id
382 )
383 );
384 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
385 }
386
387 /**
388 * Create lookup table entries for a simple (non variable) product.
389 * Assumes that no entries exist yet.
390 *
391 * @param \WC_Product $product The product to create the entries for.
392 */
393 private function create_data_for_simple_product( \WC_Product $product ) {
394 $product_attributes_data = $this->get_attribute_taxonomies( $product );
395 $has_stock = $product->is_in_stock();
396 $product_id = $product->get_id();
397 foreach ( $product_attributes_data as $taxonomy => $data ) {
398 $term_ids = $data['term_ids'];
399 foreach ( $term_ids as $term_id ) {
400 $this->insert_lookup_table_data( $product_id, $product_id, $taxonomy, $term_id, false, $has_stock );
401 }
402 }
403 }
404
405 /**
406 * Create lookup table entries for a variable product.
407 * Assumes that no entries exist yet.
408 *
409 * @param \WC_Product_Variable $product The product to create the entries for.
410 */
411 private function create_data_for_variable_product( \WC_Product_Variable $product ) {
412 $product_attributes_data = $this->get_attribute_taxonomies( $product );
413 $variation_attributes_data = array_filter(
414 $product_attributes_data,
415 function( $item ) {
416 return $item['used_for_variations'];
417 }
418 );
419 $non_variation_attributes_data = array_filter(
420 $product_attributes_data,
421 function( $item ) {
422 return ! $item['used_for_variations'];
423 }
424 );
425
426 $main_product_has_stock = $product->is_in_stock();
427 $main_product_id = $product->get_id();
428
429 foreach ( $non_variation_attributes_data as $taxonomy => $data ) {
430 $term_ids = $data['term_ids'];
431 foreach ( $term_ids as $term_id ) {
432 $this->insert_lookup_table_data( $main_product_id, $main_product_id, $taxonomy, $term_id, false, $main_product_has_stock );
433 }
434 }
435
436 $term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );
437 $variations = $this->get_variations_of( $product );
438
439 foreach ( $variation_attributes_data as $taxonomy => $data ) {
440 foreach ( $variations as $variation ) {
441 $this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product_id, $data['term_ids'], $term_ids_by_slug_cache );
442 }
443 }
444 }
445
446 /**
447 * Create all the necessary lookup data for a given variation.
448 *
449 * @param \WC_Product_Variation $variation The variation to create entries for.
450 */
451 private function create_data_for_variation( \WC_Product_Variation $variation ) {
452 $main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() );
453
454 $product_attributes_data = $this->get_attribute_taxonomies( $main_product );
455 $variation_attributes_data = array_filter(
456 $product_attributes_data,
457 function( $item ) {
458 return $item['used_for_variations'];
459 }
460 );
461
462 $term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );
463
464 foreach ( $variation_attributes_data as $taxonomy => $data ) {
465 $this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product->get_id(), $data['term_ids'], $term_ids_by_slug_cache );
466 }
467 }
468
469 /**
470 * Create lookup table entries for a given variation, corresponding to a given taxonomy and a set of term ids.
471 *
472 * @param \WC_Product_Variation $variation The variation to create entries for.
473 * @param string $taxonomy The taxonomy to create the entries for.
474 * @param int $main_product_id The parent product id.
475 * @param array $term_ids The term ids to create entries for.
476 * @param array $term_ids_by_slug_cache A dictionary of term ids by term slug, as returned by 'get_term_ids_by_slug_cache'.
477 */
478 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 ) {
479 $variation_id = $variation->get_id();
480 $variation_has_stock = $variation->is_in_stock();
481 $variation_definition_term_id = $this->get_variation_definition_term_id( $variation, $taxonomy, $term_ids_by_slug_cache );
482 if ( $variation_definition_term_id ) {
483 $this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $variation_definition_term_id, true, $variation_has_stock );
484 } else {
485 $term_ids_for_taxonomy = $term_ids;
486 foreach ( $term_ids_for_taxonomy as $term_id ) {
487 $this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $term_id, true, $variation_has_stock );
488 }
489 }
490 }
491
492 /**
493 * Get a cache of term ids by slug for a set of taxonomies, with this format:
494 *
495 * [
496 * 'taxonomy' => [
497 * 'slug_1' => id_1,
498 * 'slug_2' => id_2,
499 * ...
500 * ], ...
501 * ]
502 *
503 * @param array $taxonomies List of taxonomies to build the cache for.
504 * @return array A dictionary of taxonomies => dictionary of term slug => term id.
505 */
506 private function get_term_ids_by_slug_cache( $taxonomies ) {
507 $result = array();
508 foreach ( $taxonomies as $taxonomy ) {
509 $terms = WC()->call_function(
510 'get_terms',
511 array(
512 'taxonomy' => $taxonomy,
513 'hide_empty' => false,
514 'fields' => 'id=>slug',
515 )
516 );
517 $result[ $taxonomy ] = array_flip( $terms );
518 }
519 return $result;
520 }
521
522 /**
523 * Get the id of the term that defines a variation for a given taxonomy,
524 * or null if there's no such defining id (for variations having "Any <taxonomy>" as the definition)
525 *
526 * @param \WC_Product_Variation $variation The variation to get the defining term id for.
527 * @param string $taxonomy The taxonomy to get the defining term id for.
528 * @param array $term_ids_by_slug_cache A term ids by slug as generated by get_term_ids_by_slug_cache.
529 * @return int|null The term id, or null if there's no defining id for that taxonomy in that variation.
530 */
531 private function get_variation_definition_term_id( \WC_Product_Variation $variation, string $taxonomy, array $term_ids_by_slug_cache ) {
532 $variation_attributes = $variation->get_attributes();
533 $term_slug = ArrayUtil::get_value_or_default( $variation_attributes, $taxonomy );
534 if ( $term_slug ) {
535 return $term_ids_by_slug_cache[ $taxonomy ][ $term_slug ];
536 } else {
537 return null;
538 }
539 }
540
541 /**
542 * Get the variations of a given variable product.
543 *
544 * @param \WC_Product_Variable $product The product to get the variations for.
545 * @return array An array of WC_Product_Variation objects.
546 */
547 private function get_variations_of( \WC_Product_Variable $product ) {
548 $variation_ids = $product->get_children();
549 return array_map(
550 function( $id ) {
551 return WC()->call_function( 'wc_get_product', $id );
552 },
553 $variation_ids
554 );
555 }
556
557 /**
558 * Check if a given product is a variable product.
559 *
560 * @param \WC_Product $product The product to check.
561 * @return bool True if it's a variable product, false otherwise.
562 */
563 private function is_variable_product( \WC_Product $product ) {
564 return is_a( $product, \WC_Product_Variable::class );
565 }
566
567 /**
568 * Check if a given product is a variation.
569 *
570 * @param \WC_Product $product The product to check.
571 * @return bool True if it's a variation, false otherwise.
572 */
573 private function is_variation( \WC_Product $product ) {
574 return is_a( $product, \WC_Product_Variation::class );
575 }
576
577 /**
578 * Return the list of taxonomies used for variations on a product together with
579 * the associated term ids, with the following format:
580 *
581 * [
582 * 'taxonomy_name' =>
583 * [
584 * 'term_ids' => [id, id, ...],
585 * 'used_for_variations' => true|false
586 * ], ...
587 * ]
588 *
589 * @param \WC_Product $product The product to get the attribute taxonomies for.
590 * @return array Information about the attribute taxonomies of the product.
591 */
592 private function get_attribute_taxonomies( \WC_Product $product ) {
593 $product_attributes = $product->get_attributes();
594 $result = array();
595 foreach ( $product_attributes as $taxonomy_name => $attribute_data ) {
596 if ( ! $attribute_data->get_id() ) {
597 // Custom product attribute, not suitable for attribute-based filtering.
598 continue;
599 }
600
601 $result[ $taxonomy_name ] = array(
602 'term_ids' => $attribute_data->get_options(),
603 'used_for_variations' => $attribute_data->get_variation(),
604 );
605 }
606
607 return $result;
608 }
609
610 /**
611 * Insert one entry in the lookup table.
612 *
613 * @param int $product_id The product id.
614 * @param int $product_or_parent_id The product id for non-variable products, the main/parent product id for variations.
615 * @param string $taxonomy Taxonomy name.
616 * @param int $term_id Term id.
617 * @param bool $is_variation_attribute True if the taxonomy corresponds to an attribute used to define variations.
618 * @param bool $has_stock True if the product is in stock.
619 */
620 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 ) {
621 global $wpdb;
622
623 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
624 $wpdb->query(
625 $wpdb->prepare(
626 'INSERT INTO ' . $this->lookup_table_name . ' (
627 product_id,
628 product_or_parent_id,
629 taxonomy,
630 term_id,
631 is_variation_attribute,
632 in_stock)
633 VALUES
634 ( %d, %d, %s, %d, %d, %d )',
635 $product_id,
636 $product_or_parent_id,
637 $taxonomy,
638 $term_id,
639 $is_variation_attribute ? 1 : 0,
640 $has_stock ? 1 : 0
641 )
642 );
643 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
644 }
645
646 /**
647 * Handler for the woocommerce_rest_insert_product hook.
648 * Needed to update the lookup table when the REST API batch insert/update endpoints are used.
649 *
650 * @param WP_Post $product The post representing the created or updated product.
651 * @param \WP_REST_Request $request The REST request that caused the hook to be fired.
652 * @return void
653 */
654 private function on_product_created_or_updated_via_rest_api( WP_Post $product, \WP_REST_Request $request ): void {
655 if ( StringUtil::ends_with( $request->get_route(), '/batch' ) ) {
656 $this->on_product_changed( $product->ID );
657 }
658 }
659
660 /**
661 * Tells if a lookup table regeneration is currently in progress.
662 *
663 * @return bool True if a lookup table regeneration is already in progress.
664 */
665 public function regeneration_is_in_progress() {
666 return 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null );
667 }
668
669 /**
670 * Set a permanent flag (via option) indicating that the lookup table regeneration is in process.
671 */
672 public function set_regeneration_in_progress_flag() {
673 update_option( 'woocommerce_attribute_lookup_regeneration_in_progress', 'yes' );
674 }
675
676 /**
677 * Remove the flag indicating that the lookup table regeneration is in process.
678 */
679 public function unset_regeneration_in_progress_flag() {
680 delete_option( 'woocommerce_attribute_lookup_regeneration_in_progress' );
681 }
682
683 /**
684 * Set a flag indicating that the last lookup table regeneration process started was aborted.
685 */
686 public function set_regeneration_aborted_flag() {
687 update_option( 'woocommerce_attribute_lookup_regeneration_aborted', 'yes' );
688 }
689
690 /**
691 * Remove the flag indicating that the last lookup table regeneration process started was aborted.
692 */
693 public function unset_regeneration_aborted_flag() {
694 delete_option( 'woocommerce_attribute_lookup_regeneration_aborted' );
695 }
696
697 /**
698 * Tells if the last lookup table regeneration process started was aborted
699 * (via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option).
700 *
701 * @return bool True if the last lookup table regeneration process was aborted.
702 */
703 public function regeneration_was_aborted(): bool {
704 return 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_aborted' );
705 }
706
707 /**
708 * Check if the lookup table contains any entry at all.
709 *
710 * @return bool True if the table contains entries, false if the table is empty.
711 */
712 public function lookup_table_has_data(): bool {
713 global $wpdb;
714
715 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
716 return ( (int) $wpdb->get_var( "SELECT EXISTS (SELECT 1 FROM {$this->lookup_table_name})" ) ) !== 0;
717 }
718 }
719