defineTables(); $this->cache = Cache::for('term_ids')->connect('user'); // Register hooks add_action('set_object_terms', [$this, 'handleTermAssignment'], 10, 6); // Add filter for bulk operation handling add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3); } public function defineTables():void { $userIndex = CustomTable::for('user_term_index'); $userIndex->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'user_id' => "{$userIndex->getUserIDType()} NOT NULL", 'term_id' => "{$userIndex->getTermIDType()} NOT NULL", 'taxonomy' => 'varchar(32) NOT NULL', 'post_count' => 'int(11) NOT NULL DEFAULT 1', 'parent_id' => "{$userIndex->getTermIDType()} NOT NULL DEFAULT 0", 'last_used' => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', ]); $userIndex->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], ['key' => 'UNIQUE', 'value' => '`user_term` (`user_id`, `term_id`, `taxonomy`)'], '`user_taxonomy` (`user_id`, `taxonomy`)', '`taxonomy` (`taxonomy`)', '`user_id` (`user_id`)', '`term_id` (`term_id`)', '`parent_id` (`parent_id`)' ]); $base = BASE; $userIndex->setConstraints([ "CONSTRAINT `{$base}user_term_user_fk` FOREIGN KEY (`user_id`) REFERENCES `{$userIndex->getUserTable()}` (`ID`) ON DELETE CASCADE", "CONSTRAINT `{$base}user_term_term_fk` FOREIGN KEY (`term_id`) REFERENCES `{$userIndex->getTermTable()}` (`term_id`) ON DELETE CASCADE" ]); $userIndex->defineTable(); $this->index = $userIndex; } /** * @param int $user_id * @param string|null $taxonomy * * @return void */ public function clearUserCache(int $user_id, string|null $taxonomy = null):void { $cache = Cache::for($user_id.'_term_relationships', DAY_IN_SECONDS)->connect('post', true)->connect('taxonomy', true); $cache->flush(); } /** * Handle term assignment changes * @param int $object_id * @param array $terms * @param array $tt_ids * @param string $taxonomy * @param bool $append * @param array $old_tt_ids * * @return void */ public function handleTermAssignment(int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids):void { $post = get_post($object_id); // Skip if not a valid post if (!$post || !str_starts_with($post->post_type, BASE)) { return; } $user_id = $post->post_author; $termIDs = array_unique(array_merge($tt_ids, $old_tt_ids)); if (!empty($termIDs)) { foreach ($termIDs as $termID) { $term_id = $this->getTermIDFromTTID($termID); $this->updateUserTerm($user_id, $term_id, $taxonomy); } } } /** * Updates the entry for the user's term relationship * @param int $user_id * @param int $term_id * @param string $taxonomy * * @return bool */ public function updateUserTerm(int $user_id, int $term_id, string $taxonomy):bool { $taxonomy = jvbCheckBase($taxonomy); // Ensure the term exists $term = get_term($term_id, $taxonomy); if (is_wp_error($term) || empty($term)) { return false; } $posts = new WP_Query([ 'post_author' => $user_id, 'tax_query' => [ [ 'taxonomy' => $taxonomy, 'terms' => $term_id, ] ], 'post_status' => 'publish', 'posts_per_page'=> -1, 'fields' => 'ids' ]); $userPosts = count($posts->posts); $result = $this->index->findOrCreate([ 'user_id' => $user_id, 'term_id' => $term_id, 'taxonomy' => $taxonomy ],[ 'parent_id' => $term->parent, 'post_count'=> $userPosts ]); if ($term->parent > 0) { $this->updateUserTerm($user_id, $term->parent, $taxonomy); } return (bool) $result; } /** * Helper function to get term_id from term_taxonomy_id * @param int $tt_id * * @return int */ private function getTermIDFromTTID(int $tt_id):int { global $wpdb; return $wpdb->get_var($wpdb->prepare( "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = %d", $tt_id )); } /** * Rebuild the user terms index for all users * @return array */ public function rebuildAllUserIndex():array { // Clear existing index global $wpdb; $wpdb->query("TRUNCATE TABLE {$this->index->getFullTableName()}"); $users = get_users([ 'has_published_posts' => [array_map('jvbCheckBase', Registrar::getRegistered('post'))], 'fields' => 'ID' ]); if (empty($users)) { return [ 'success' => true, 'message' => 'No users found to update' ]; } JVB()->queue()->queueOperation( 'rebuild_user_term_index', 0, [ 'users' => $users ], [ 'chunk_key' => 'users', 'chunk_size' => 5, 'operation_id' => 'rebuild_user_terms_' . date('Y_m_d') ] ); return [ 'success' => true, 'message' => 'Operation Queued for Processing' ]; } /** * Rebuild the index for a specific user * @param int $user_id * * @return array */ public function rebuildUserIndex(int $user_id):array { $user = get_userdata($user_id); if (!$user) { return [ 'success' => false, 'message' => 'User does not exist' ]; } JVB()->queue()->queueOperation( 'rebuild_user_term_index', 0, [ 'users' => [$user_id] ], [ 'count' => 1, 'operation_id' => 'rebuild_user_'.$user_id.'_terms_' . date('Y_m_d') ] ); return [ 'success' => true, 'message' => 'Operation Queued for Processing' ]; } /** * Handle bulk operations for notifications * * @param WP_Error|array $result Default result * @param object $operation Operation object * @param array $data Current item * * @return WP_Error|array|bool Operation result */ public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array|bool { if ($operation->type !== 'rebuild_user_term_index') { return $result; } try { $results = []; foreach ($data['users'] as $user_id) { $this->index->delete(['user_id' => $user_id]); // Clear all caches for this user $this->clearUserCache($user_id); // Get all the user's published posts $posts = new WP_Query([ 'post_status' => 'publish', 'post_type' => array_map('jvbCheckBase', Registrar::getRegistered('post')), 'posts_per_page'=> -1, 'fields' => 'ids' ]); if (empty($posts->posts)) { return [ 'success' => true, 'result' => [ 'user_id' => $user_id, 'processed_posts'=> 0, 'processed_terms'=> 0, ] ]; } $terms = []; $taxonomies = []; $result=[ 'user_id' => $user_id, 'posts' => count($posts->posts) ]; foreach ($posts->posts as $postID) { $postType = get_post_type($postID); if (!array_key_exists($postType, $taxonomies)) { $taxonomies[$postType] = get_object_taxonomies($postType); } $tax = $taxonomies[$postType]; $terms = array_unique(array_merge($terms, wp_get_object_terms($postID, $tax, ['fields' => 'ids']))); } $result['terms'] = count($terms); foreach ($terms as $termID) { $taxonomy = get_term($termID)->taxonomy??false; if (!$taxonomy) { continue; } $this->updateUserTerm($user_id, $termID, $taxonomy); } $results[] = $result; } return [ 'success' => true, 'result' => $results ]; } catch (Exception $e) { JVB()->error()->log( '[UserTermsManager]:processOperation', "Exception during operation processing: " . $e->getMessage(), [ 'operation' => $operation->id, ] ); return [ 'success' => false, 'result' => $e->getMessage() ]; } } /** * @param int $user_id * @param string $taxonomy * @param array $args * * @return array */ public function fetchUserTerms(int $user_id, string $taxonomy, array $args = []):array { $taxonomy = jvbNoBase($taxonomy); if (!in_array($taxonomy, Registrar::getRegistered('term'))){ return []; } $taxonomy = jvbCheckBase($taxonomy); $order = array_key_exists('order', $args) ? (!in_array(strtoupper($args['order']), ['ASC', 'DESC']) ? 'DESC' : $args['order']) : 'DESC'; $orderby = array_key_exists('orderby', $args) ? (!in_array(strtolower($args['orderby']), ['post_count', 'last_used']) ? 'post_count' : $args['orderby']) : 'post_count'; $limit = array_key_exists('limit', $args) ? absint($args['limit']) : 0; $limit = $limit === 0 ? null : $limit; return $this->index->pluck( 'term_id', [ 'user_id' => $user_id, 'taxonomy' => $taxonomy ], $orderby, $order, $limit ); } }