table_name = $wpdb->prefix . BASE . 'news_relationships'; $this->cache = Cache::for('news_relationships', WEEK_IN_SECONDS)->connect('post', true)->connect('taxonomy', true)->connect('user',true); // Register hooks add_action('init', [$this, 'registerHooks']); add_filter(BASE . 'handle_bulk_operation', [ $this, 'processNewsStatsRebuild' ], 10, 3); } /** * Register all necessary hooks * @return void */ public function registerHooks() { // Track new/updated news posts add_action('save_post_' . BASE . 'news', [$this, 'handleUpdate'], 10, 3); add_action('before_delete_post', [$this, 'handleDelete'], 10, 2); // Track artist-shop relationships add_action('set_object_terms', [$this, 'handleArtistShopChange'], 10, 6); add_action('added_term_relationship', [$this, 'handleTermRelationshipChange'], 10, 3); add_action('deleted_term_relationship', [$this, 'handleTermRelationshipChange'], 10, 3); // Track user-artist linkages add_action('added_post_meta', [$this, 'handleLinkChange'], 10, 4); add_action('updated_post_meta', [$this, 'handleLinkChange'], 10, 4); add_action('deleted_post_meta', [$this, 'handleLinkChange'], 10, 4); // Track when a shop term is deleted add_action('delete_term', [$this, 'handleShopDeletion'], 10, 4); } /** * Handle when a news post is saved or updated * @param int $post_id * @param WP_Post $post * @param bool $update * * @return void */ public function handleUpdate(int $post_id, WP_Post $post, bool $update):void { // Only proceed for published posts if ($post->post_status != 'publish') { return; } $user_id = $post->post_author; // Get linked artist $artist_id = $this->getArtistForUser($user_id); if (!$artist_id) { return; // No artist found for this user } // Get shop(s) for this artist $shops = $this->getShopsForArtist($artist_id); if (empty($shops)) { return; // No shops found for this artist } // Update counts for all shops foreach ($shops as $shop_id) { $this->updateUserShopCount($user_id, $shop_id, $artist_id); } } /** * Handle when a news post is deleted * @param int $post_id * @param WP_Post $post * * @return void */ public function handleDelete(int $post_id, WP_Post $post):void { // Only proceed for news posts if ($post->post_type !== BASE . 'news') { return; } $user_id = $post->post_author; // Find artist and shops $artist_id = $this->getArtistForUser($user_id); if (!$artist_id) { return; } $shops = $this->getShopsForArtist($artist_id); if (empty($shops)) { return; } // Update counts for all shops foreach ($shops as $shop_id) { $this->updateUserShopCount($user_id, $shop_id, $artist_id); } } /** * Handle when an artist's shop terms change * @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 handleArtistShopChange(int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids):void { // Only proceed for shop taxonomy if ($taxonomy !== BASE . 'shop') { return; } // Check if this is an artist post $post_type = get_post_type($object_id); if ($post_type !== BASE . 'artist') { return; } // Get user linked to this artist $user_id = $this->getUserForArtist($object_id); if (!$user_id) { return; } // Get added shops $added_tt_ids = array_diff($tt_ids, $old_tt_ids); $removed_tt_ids = array_diff($old_tt_ids, $tt_ids); // Update counts for added shops foreach ($added_tt_ids as $tt_id) { $term = get_term_by('term_taxonomy_id', $tt_id); if ($term) { $this->updateUserShopCount($user_id, $term->term_id, $object_id); } } // Update counts for removed shops foreach ($removed_tt_ids as $tt_id) { $term = get_term_by('term_taxonomy_id', $tt_id); if ($term) { $this->remove_user_from_shop($user_id, $term->term_id); } } } /** * Handle when a term relationship changes * @param int $object_id * @param int $tt_id * @param string $taxonomy * * @return void */ public function handleTermRelationshipChange(int $object_id, int $tt_id, string $taxonomy):void { // We need to check the taxonomy from the term taxonomy ID $term_taxonomy = get_term_by('term_taxonomy_id', $tt_id); if (!$term_taxonomy || $term_taxonomy->taxonomy !== BASE . 'shop') { return; } // Check if this is an artist post $post_type = get_post_type($object_id); if ($post_type !== BASE . 'artist') { return; } // Get user linked to this artist $user_id = $this->getUserForArtist($object_id); if (!$user_id) { return; } // Update count for the shop $this->updateUserShopCount($user_id, $term_taxonomy->term_id, $object_id); } /** * Handle when user-artist link changes * @param int $meta_id * @param int $object_id * @param string $meta_key * @param string|null $meta_value * * @return void */ public function handleLinkChange(int $meta_id, int $object_id, string $meta_key, string|null $meta_value = null):void { // Only proceed for BASE.'profile_link' meta key if ($meta_key !== BASE . 'link') { return; } // Determine if this is a user meta or post meta $is_user_meta = metadata_exists('user', $object_id, $meta_key); if ($is_user_meta) { // This is a user meta - get the artist ID from meta_value $artist_id = $meta_value; $user_id = $object_id; } else { // This is a post meta - get the user ID from meta_value $artist_id = $object_id; $user_id = $meta_value; } // Verify we have both user and artist if (!$user_id || !$artist_id) { return; } // Get shops for the artist $shops = $this->getShopsForArtist($artist_id); // Update counts for all shops foreach ($shops as $shop_id) { $this->updateUserShopCount($user_id, $shop_id, $artist_id); } } /** * Handle when a shop term is deleted * @param int $term_id * @param int $tt_id * @param string $taxonomy * @param WP_Term $deleted_term * * @return void */ public function handleShopDeletion(int $term_id, int $tt_id, string $taxonomy, WP_Term $deleted_term) { // Only proceed for shop taxonomy if ($taxonomy !== BASE . 'shop') { return; } // Remove all entries for this shop $this->removeShopEntries($term_id); } /** * Get the artist post for a user * @param int $user_id * * @return int */ public function getArtistForUser(int $user_id):int { // First try the user meta approach $artist_id = get_user_meta($user_id, BASE . 'link', true); // If not found, try the post meta approach if ($artist_id === '') { global $wpdb; $meta_key = BASE . 'link'; $query = $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %d AND post_id IN (SELECT ID FROM $wpdb->posts WHERE post_type = %s)", $meta_key, $user_id, BASE . 'artist' ); $artist_id = $wpdb->get_var($query); } return $artist_id; } /** * Get the user for an artist post * @param int $artist_id * * @return int */ public function getUserForArtist(int $artist_id):int { // First try the post meta approach $user_id = get_post_meta($artist_id, BASE . 'link', true); // If not found, try the user meta approach if ($user_id === '') { global $wpdb; $meta_key = BASE . 'link'; $query = $wpdb->prepare( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s AND meta_value = %d", $meta_key, $artist_id ); $user_id = $wpdb->get_var($query); } return $user_id; } /** * Get news count for a specific artist, by jvb_artist ID * @param int $artist_id * * @return int */ public function getArtistNewsCount(int $artist_id):int { global $wpdb; // Get news_count from first entry for this user // Since the count should be the same across all shop entries return $wpdb->get_var($wpdb->prepare( "SELECT news_count FROM {$this->table_name} WHERE artist_id = %d LIMIT 1", $artist_id )); } /** * Get news count for a specific artist, by jvb_artist ID * @param int $user_id * * @return int */ public function getUserNewsCount(int $user_id):int { global $wpdb; // Get news_count from first entry for this user // Since the count should be the same across all shop entries return $wpdb->get_var($wpdb->prepare( "SELECT news_count FROM {$this->table_name} WHERE user_id = %d LIMIT 1", $user_id )); } /** * Get all artists with news post counts * @return array */ public function getAllArtistsWithNews():int { global $wpdb; // Use DISTINCT to get unique artist/user pairs // We just need one row per artist since the news count is the same for all their shop entries $results = $wpdb->get_results(" SELECT DISTINCT user_id, artist_id, news_count FROM {$this->table_name} WHERE artist_id IS NOT NULL AND news_count > 0 ORDER BY news_count DESC "); $seen_artists = []; $artists = []; foreach ($results as $row) { // Skip if we've already seen this artist (avoiding duplicates from multiple shop entries) if (isset($seen_artists[$row->artist_id])) { continue; } $artist = get_post($row->artist_id); if (!$artist) { continue; } $seen_artists[$row->artist_id] = true; $artists[] = [ 'id' => $row->user_id, 'name' => $artist->post_title, 'count' => (int)$row->news_count ]; } //Sort by artist name usort($artists, function ($a, $b) { return strcasecmp($a['name'], $b['name']); }); return $artists; } /** * Get shops for an artist * @param int $artist_id * * @return array */ public function getShopsForArtist(int $artist_id):array { $shop_terms = wp_get_object_terms($artist_id, BASE . 'shop', ['fields' => 'ids']); if (is_wp_error($shop_terms) || empty($shop_terms)) { return []; } return $shop_terms; } /** * Count news posts for a user * @param int $user_id * * @return int */ public function countUserNewsPosts(int $user_id):int { $args = [ 'post_type' => BASE . 'news', 'post_status' => 'publish', 'author' => $user_id, 'posts_per_page' => -1, 'fields' => 'ids', ]; $query = new WP_Query($args); return $query->found_posts; } /** * Update news count for a user-shop pair * @param int $user_id * @param int $shop_id * @param int $artist_id * * @return void */ public function updateUserShopCount(int $user_id, int $shop_id, int $artist_id):void { global $wpdb; // Get news count for this user $news_count = $this->countUserNewsPosts($user_id); // Get the last post date $last_post = get_posts([ 'post_type' => BASE . 'news', 'post_status' => 'publish', 'author' => $user_id, 'posts_per_page' => 1, 'orderby' => 'date', 'order' => 'DESC' ]); $last_post_date = !empty($last_post) ? $last_post[0]->post_date : null; // Check if entry exists $existing = $wpdb->get_row($wpdb->prepare( "SELECT id FROM {$this->table_name} WHERE shop_id = %d AND user_id = %d", $shop_id, $user_id )); if ($existing) { // Update existing entry $wpdb->update( $this->table_name, [ 'news_count' => $news_count, 'last_post_date' => $last_post_date, 'artist_id' => $artist_id ], [ 'shop_id' => $shop_id, 'user_id' => $user_id ] ); } elseif ($news_count > 0) { // Only create entry if there are news posts $wpdb->insert( $this->table_name, [ 'shop_id' => $shop_id, 'user_id' => $user_id, 'artist_id' => $artist_id, 'news_count' => $news_count, 'last_post_date' => $last_post_date ] ); } // Update cache $this->cache->forget($shop_id); // Update shop total count $this->updateShopTotal($shop_id); } /** * Remove all entries for a shop * @param int $shop_id * * @return void */ public function removeShopEntries(int $shop_id):void { global $wpdb; $wpdb->delete( $this->table_name, ['shop_id' => $shop_id] ); // Update cache $this->cache->forget($shop_id); } /** * Update total news count for a shop * @param int $shop_id * * @return void */ public function updateShopTotal(int $shop_id):void { global $wpdb; // Calculate the total $total = $wpdb->get_var($wpdb->prepare( "SELECT SUM(news_count) FROM {$this->table_name} WHERE shop_id = %d", $shop_id )); // Store in term meta update_term_meta($shop_id, BASE . 'news_count', (int)$total); } /** * Get news stats for a shop * @param int $shop_id * * @return array */ public function getShopNewsStats(int $shop_id):array { $cached = $this->cache->get($shop_id); if ($cached !== false) { return $cached; } global $wpdb; $stats = $wpdb->get_results($wpdb->prepare( "SELECT s.*, u.display_name as user_name, p.post_title as artist_name FROM {$this->table_name} s LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID LEFT JOIN {$wpdb->posts} p ON s.artist_id = p.ID WHERE s.shop_id = %d ORDER BY s.news_count DESC", $shop_id )); $total = $wpdb->get_var($wpdb->prepare( "SELECT SUM(news_count) FROM {$this->table_name} WHERE shop_id = %d", $shop_id )); $result = [ 'total' => (int)$total, 'artists' => $stats ]; $this->cache->set($shop_id, $result); return $result; } /** * Rebuild all shop news stats (useful for migration) * @return array * TODO: Add Admin page */ public function rebuildAllStats():array { // Get all artists with shops $artists = get_posts([ 'post_type' => BASE . 'artist', 'post_status' => ['draft', 'publish'], 'posts_per_page' => -1, 'fields' => 'ids', 'tax_query' => [ [ 'taxonomy' => BASE . 'shop', 'operator' => 'EXISTS' ] ] ]); global $wpdb; // Clear existing data $wpdb->query("TRUNCATE TABLE {$this->table_name}"); JVB()->queue()->queueOperation( 'rebuild_news', 0, [ 'artists' => $artists, ], [ 'count' => count($artists), 'operation_id' => 'rebuild_news_'.uniqid() ] ); return [ 'success' => true, 'message' => 'Operation Queued for Processing' ]; } /** * @param WP_Error $result * @param object $operation * @param array $data * * @return WP_Error|array */ public function processNewsStatsRebuild(WP_Error|array $result, object $operation, array $data):WP_Error|array { if ($operation->type !== 'rebuild_news') { return $result; } $shopIDs = []; foreach ($data['artists'] as $artist_id) { // Get linked user $user_id = $this->getUserForArtist($artist_id); if (!$user_id) { continue; } // Get shops $shops = $this->getShopsForArtist($artist_id); if (empty($shops)) { continue; } // Update counts for each shop foreach ($shops as $shop_id) { $shopIDs[] = $shop_id; $this->updateUserShopCount($user_id, $shop_id, $artist_id); } } return [ 'success' => true, 'result' => [ 'artists_processed' => count($data['artists']), 'shops_updated' => $shopIDs ] ]; } /** * Get news posts for a specific shop * * @param int $shop_id The shop term ID * @return array Array of news post IDs */ public function getShopArtistsWithNews(int $shop_id):array { global $wpdb; // Get all users (authors) associated with this shop $user_ids = $wpdb->get_col($wpdb->prepare( "SELECT user_id FROM {$this->table_name} WHERE shop_id = %d AND news_count > 0", $shop_id )); if (empty($user_ids)) { return [0]; } return $user_ids; } /** * Get total news count for all shops (for dashboard) * @return array */ public function getAllShopsNews():array { $cache_key = 'all'; $cached = $this->cache->get($cache_key); if ($cached !== false) { return $cached; } global $wpdb; // Get unique shop IDs with their news counts directly from our table $shop_stats = $wpdb->get_results(" SELECT shop_id, SUM(news_count) as total_count FROM {$this->table_name} GROUP BY shop_id HAVING total_count > 0 "); $result = []; if (!empty($shop_stats)) { // Extract all shop IDs $shop_ids = array_map(function ($stat) { return $stat->shop_id; }, $shop_stats); // Get all terms in one query $shops = get_terms([ 'taxonomy' => BASE . 'shop', 'include' => $shop_ids, 'hide_empty' => false ]); if (!is_wp_error($shops)) { // Create a lookup array for easy access $shops_by_id = []; foreach ($shops as $shop) { $shops_by_id[$shop->term_id] = $shop; } // Build the result array foreach ($shop_stats as $stat) { if (isset($shops_by_id[$stat->shop_id])) { $result[] = [ 'id' => $stat->shop_id, 'name' => $shops_by_id[$stat->shop_id]->name, 'count' => (int)$stat->total_count ]; } } // Sort by count (highest first) usort($result, function ($a, $b) { return strcasecmp($a['name'], $b['name']); }); } } $this->cache->set($cache_key, $result); return $result; } }