<?php
|
namespace JVBase\managers;
|
|
use JVBase\JVB;
|
use WP_Post;
|
use WP_Term;
|
use WP_Query;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
/**
|
* Tracks and manages news post counts for shops based on artist relationships
|
*/
|
class NewsRelationships
|
{
|
private string $table_name;
|
private object $cache;
|
|
public function __construct()
|
{
|
global $wpdb;
|
$this->table_name = $wpdb->prefix . BASE . 'news_relationships';
|
$this->cache = new CacheManager('news_relationships', 3600); // 1 hour cache by default
|
|
// 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.'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->invalidate('shop_' . $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->invalidate('shop_' . $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
|
{
|
$cache_key = 'shop_' . $shop_id;
|
$cached = $this->cache->get($cache_key);
|
|
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($cache_key, $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_shops_counts';
|
$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;
|
}
|
}
|