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 |