2019-10-01 23:35:37 +00:00
< ? php
/**
* Keeps the product category lookup table in sync with live data .
*/
2022-02-17 19:14:23 +00:00
namespace Automattic\WooCommerce\Internal\Admin ;
use Automattic\WooCommerce\Internal\Admin\Install ;
2019-10-01 23:35:37 +00:00
defined ( 'ABSPATH' ) || exit ;
/**
2022-02-17 19:14:23 +00:00
* \Automattic\WooCommerce\Internal\Admin\CategoryLookup class .
2019-10-01 23:35:37 +00:00
*/
class CategoryLookup {
/**
* Stores changes to categories we need to sync .
*
* @ var array
*/
protected $edited_product_cats = array ();
/**
* The single instance of the class .
*
* @ var object
*/
protected static $instance = null ;
/**
* Constructor
*
* @ return void
*/
protected function __construct () {}
/**
* Get class instance .
*
* @ return object Instance .
*/
final public static function instance () {
if ( null === static :: $instance ) {
static :: $instance = new static ();
}
return static :: $instance ;
}
/**
* Init hooks .
*/
public function init () {
add_action ( 'generate_category_lookup_table' , array ( $this , 'regenerate' ) );
add_action ( 'edit_product_cat' , array ( $this , 'before_edit' ), 99 );
add_action ( 'edited_product_cat' , array ( $this , 'on_edit' ), 99 );
2021-07-08 17:39:12 +00:00
add_action ( 'created_product_cat' , array ( $this , 'on_create' ), 99 );
2019-10-01 23:35:37 +00:00
}
/**
* Regenerate all lookup table data .
*/
public function regenerate () {
global $wpdb ;
// Delete existing data and ensure schema is current.
Install :: create_tables ();
$wpdb -> query ( " TRUNCATE TABLE $wpdb->wc_category_lookup " );
$terms = get_terms (
'product_cat' ,
array (
'hide_empty' => false ,
'fields' => 'id=>parent' ,
)
);
$hierarchy = array ();
$inserts = array ();
$this -> unflatten_terms ( $hierarchy , $terms , 0 );
$this -> get_term_insert_values ( $inserts , $hierarchy );
if ( ! $inserts ) {
return ;
}
$insert_string = implode (
'),(' ,
array_map (
function ( $item ) {
return implode ( ',' , $item );
},
$inserts
)
);
2020-08-11 19:18:47 +00:00
$wpdb -> query ( " INSERT IGNORE INTO $wpdb->wc_category_lookup (category_tree_id,category_id) VALUES ( { $insert_string } ) " ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2019-10-01 23:35:37 +00:00
}
/**
* Store edits so we know when the parent ID changes .
*
* @ param int $category_id Term ID being edited .
*/
public function before_edit ( $category_id ) {
$category = get_term ( $category_id , 'product_cat' );
$this -> edited_product_cats [ $category_id ] = $category -> parent ;
}
/**
* When a product category gets edited , see if we need to sync the table .
*
* @ param int $category_id Term ID being edited .
*/
public function on_edit ( $category_id ) {
global $wpdb ;
if ( ! isset ( $this -> edited_product_cats [ $category_id ] ) ) {
return ;
}
$category_object = get_term ( $category_id , 'product_cat' );
$prev_parent = $this -> edited_product_cats [ $category_id ];
$new_parent = $category_object -> parent ;
// No edits - no need to modify relationships.
if ( $prev_parent === $new_parent ) {
return ;
}
$this -> delete ( $category_id , $prev_parent );
$this -> update ( $category_id );
}
2021-07-08 17:39:12 +00:00
/**
* When a product category gets created , add a new lookup row .
*
* @ param int $category_id Term ID being created .
*/
public function on_create ( $category_id ) {
2021-07-23 13:36:02 +00:00
// If WooCommerce is being installed on a multisite, lookup tables haven't been created yet.
if ( 'yes' === get_transient ( 'wc_installing' ) ) {
return ;
}
2021-07-08 17:39:12 +00:00
$this -> update ( $category_id );
}
2019-10-01 23:35:37 +00:00
/**
* Delete lookup table data from a tree .
*
* @ param int $category_id Category ID to delete .
* @ param int $category_tree_id Tree to delete from .
* @ return void
*/
protected function delete ( $category_id , $category_tree_id ) {
global $wpdb ;
if ( ! $category_tree_id ) {
return ;
}
$ancestors = get_ancestors ( $category_tree_id , 'product_cat' , 'taxonomy' );
$ancestors [] = $category_tree_id ;
$children = get_term_children ( $category_id , 'product_cat' );
$children [] = $category_id ;
$id_list = implode ( ',' , array_map ( 'intval' , array_unique ( array_filter ( $children ) ) ) );
foreach ( $ancestors as $ancestor ) {
2020-08-11 19:18:47 +00:00
$wpdb -> query ( $wpdb -> prepare ( " DELETE FROM $wpdb->wc_category_lookup WHERE category_tree_id = %d AND category_id IN ( { $id_list } ) " , $ancestor ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2019-10-01 23:35:37 +00:00
}
}
/**
* Updates lookup table data for a category by ID .
*
* @ param int $category_id Category ID to update .
*/
protected function update ( $category_id ) {
global $wpdb ;
2021-09-28 16:59:10 +00:00
$ancestors = get_ancestors ( $category_id , 'product_cat' , 'taxonomy' );
$children = get_term_children ( $category_id , 'product_cat' );
$inserts = array ();
$inserts [] = $this -> get_insert_sql ( $category_id , $category_id );
$children_ids = array_map ( 'intval' , array_unique ( array_filter ( $children ) ) );
2019-10-01 23:35:37 +00:00
foreach ( $ancestors as $ancestor ) {
$inserts [] = $this -> get_insert_sql ( $category_id , $ancestor );
2021-09-28 16:59:10 +00:00
foreach ( $children_ids as $child_category_id ) {
$inserts [] = $this -> get_insert_sql ( $child_category_id , $ancestor );
2019-10-01 23:35:37 +00:00
}
}
$insert_string = implode ( ',' , $inserts );
2020-08-11 19:18:47 +00:00
$wpdb -> query ( " INSERT IGNORE INTO $wpdb->wc_category_lookup (category_id, category_tree_id) VALUES { $insert_string } " ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
2019-10-01 23:35:37 +00:00
}
/**
* Get category lookup table values to insert .
*
* @ param int $category_id Category ID to insert .
* @ param int $category_tree_id Tree to insert into .
* @ return string
*/
protected function get_insert_sql ( $category_id , $category_tree_id ) {
global $wpdb ;
return $wpdb -> prepare ( '(%d,%d)' , $category_id , $category_tree_id );
}
/**
* Used to construct insert query recursively .
*
* @ param array $inserts Array of data to insert .
* @ param array $terms Terms to insert .
* @ param array $parents Parent IDs the terms belong to .
*/
protected function get_term_insert_values ( & $inserts , $terms , $parents = array () ) {
foreach ( $terms as $term ) {
$insert_parents = array_merge ( array ( $term [ 'term_id' ] ), $parents );
foreach ( $insert_parents as $parent ) {
$inserts [] = array (
$parent ,
$term [ 'term_id' ],
);
}
$this -> get_term_insert_values ( $inserts , $term [ 'descendants' ], $insert_parents );
}
}
/**
* Convert flat terms array into nested array .
*
* @ param array $hierarchy Array to put terms into .
* @ param array $terms Array of terms ( id => parent ) .
* @ param integer $parent Parent ID .
*/
protected function unflatten_terms ( & $hierarchy , & $terms , $parent = 0 ) {
foreach ( $terms as $term_id => $parent_id ) {
if ( ( int ) $parent_id === $parent ) {
$hierarchy [ $term_id ] = array (
'term_id' => $term_id ,
'descendants' => array (),
);
unset ( $terms [ $term_id ] );
}
}
foreach ( $hierarchy as $term_id => $terms_array ) {
$this -> unflatten_terms ( $hierarchy [ $term_id ][ 'descendants' ], $terms , $term_id );
}
}
/**
* Get category descendants .
*
* @ param int $category_id The category ID to lookup .
* @ return array
*/
protected function get_descendants ( $category_id ) {
global $wpdb ;
return wp_parse_id_list (
$wpdb -> get_col (
$wpdb -> prepare (
" SELECT category_id FROM $wpdb->wc_category_lookup WHERE category_tree_id = %d " ,
$category_id
)
)
);
}
/**
* Return all ancestor category ids for a category .
*
* @ param int $category_id The category ID to lookup .
* @ return array
*/
protected function get_ancestors ( $category_id ) {
global $wpdb ;
return wp_parse_id_list (
$wpdb -> get_col (
$wpdb -> prepare (
" SELECT category_tree_id FROM $wpdb->wc_category_lookup WHERE category_id = %d " ,
$category_id
)
)
);
}
}