<?php
|
namespace JVBase\managers;
|
|
use JVBase\managers\Cache;
|
use WP_Error;
|
use WP_Post;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
class TaxonomyRelationships
|
{
|
private string $table_name;
|
protected object $cache;
|
|
|
public function __construct()
|
{
|
global $wpdb;
|
$this->table_name = $wpdb->prefix . BASE.'taxonomy_relationships';
|
$this->cache = Cache::for('term_relationship', WEEK_IN_SECONDS);
|
|
// Ensure the table exists
|
// $this->create_table_if_not_exists();
|
|
|
add_action('init', [$this, 'init']);
|
}
|
|
/**
|
* @return string
|
*/
|
public function getTableName():string
|
{
|
return $this->table_name;
|
}
|
|
/**
|
* Hook into term and post saves
|
* @return void
|
*/
|
public function init():void
|
{
|
add_action('save_post', [$this, 'updatePostRelationships'], 10, 2);
|
add_action('before_delete_post', [$this, 'updatePostRelationships'], 10, 2);
|
add_action('delete_term', [$this, 'deleteTermRelationships']);
|
|
add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
|
}
|
|
|
// Update relationships for a post
|
|
/**
|
* @param int $post_id
|
*
|
* @return void
|
*/
|
public function updatePostRelationships(int $post_id, WP_Post $post):void
|
{
|
$post_type = $post->post_type;
|
if (in_array($post_type, jvbIgnoredPostTypes())) {
|
return;
|
}
|
// Get all taxonomies for this post type
|
$taxonomies = get_object_taxonomies($post_type);
|
|
if (empty($taxonomies)) {
|
return;
|
}
|
|
// Get terms for each taxonomy
|
$taxonomy_terms = [];
|
foreach ($taxonomies as $taxonomy) {
|
$terms = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
|
|
if (!is_wp_error($terms) && !empty($terms)) {
|
$taxonomy_terms[$taxonomy] = $terms;
|
|
// Add parent terms for hierarchical taxonomies
|
if (is_taxonomy_hierarchical($taxonomy)) {
|
foreach ($terms as $term_id) {
|
$ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy');
|
if (!empty($ancestors)) {
|
$taxonomy_terms[$taxonomy] = array_merge($taxonomy_terms[$taxonomy], $ancestors);
|
}
|
}
|
// Remove duplicates
|
$taxonomy_terms[$taxonomy] = array_unique($taxonomy_terms[$taxonomy]);
|
}
|
}
|
}
|
|
// Create relationships between all taxonomy terms
|
foreach ($taxonomy_terms as $taxonomy => $terms) {
|
foreach ($terms as $term_id) {
|
foreach ($taxonomy_terms as $related_taxonomy => $related_terms) {
|
if ($taxonomy === $related_taxonomy) {
|
continue; // Skip same taxonomy relationships
|
}
|
|
foreach ($related_terms as $related_term_id) {
|
$this->updateRelationship((int)$term_id, (int)$related_term_id, $taxonomy, $related_taxonomy);
|
}
|
}
|
}
|
}
|
}
|
|
|
/**
|
* @param int $term_id
|
* @param int $related_term_id
|
* @param string $taxonomy
|
* @param string $related_taxonomy
|
*
|
* @return bool
|
*/
|
public function updateRelationship(int $term_id, int $related_term_id, string $taxonomy, string $related_taxonomy):bool
|
{
|
global $wpdb;
|
|
// Make sure both terms exist
|
$term = get_term($term_id, $taxonomy);
|
$related_term = get_term($related_term_id, $related_taxonomy);
|
|
if (is_wp_error($term) || is_wp_error($related_term) || empty($term) || empty($related_term)) {
|
// One or both terms don't exist, so don't create a relationship
|
return false;
|
}
|
|
// Ensure term_id and related_term_id are integers
|
$term_id = (int)$term_id;
|
$related_term_id = (int)$related_term_id;
|
|
// Check if relationship exists
|
$existing = $wpdb->get_row($wpdb->prepare(
|
"SELECT id, post_count FROM {$this->table_name}
|
WHERE term_id = %d AND related_term_id = %d
|
AND taxonomy = %s AND related_taxonomy = %s",
|
$term_id,
|
$related_term_id,
|
$taxonomy,
|
$related_taxonomy
|
));
|
|
// Calculate number of shared posts
|
$shared_posts_count = $this->countSharedPosts($term_id, $related_term_id, $taxonomy, $related_taxonomy);
|
|
// Check if term is parent of related term
|
$is_hierarchical = 0;
|
if ($taxonomy === $related_taxonomy) {
|
$ancestors = get_ancestors($related_term_id, $taxonomy, 'taxonomy');
|
if (in_array($term_id, $ancestors)) {
|
$is_hierarchical = 1;
|
}
|
}
|
|
if ($existing) {
|
// Update existing relationship
|
$wpdb->update(
|
$this->table_name,
|
[
|
'post_count' => $shared_posts_count,
|
'is_hierarchical' => $is_hierarchical
|
],
|
[
|
'id' => $existing->id
|
]
|
);
|
} else {
|
// Insert new relationship
|
$wpdb->insert(
|
$this->table_name,
|
[
|
'term_id' => $term_id,
|
'related_term_id' => $related_term_id,
|
'taxonomy' => $taxonomy,
|
'related_taxonomy' => $related_taxonomy,
|
'post_count' => $shared_posts_count,
|
'is_hierarchical' => $is_hierarchical
|
]
|
);
|
}
|
|
return true;
|
}
|
|
/**
|
* @param int $term_id
|
* @param int $related_term_id
|
* @param string $taxonomy
|
* @param string $related_taxonomy
|
*
|
* @return int
|
*/
|
private function countSharedPosts(int $term_id, int $related_term_id, string $taxonomy, string $related_taxonomy):int
|
{
|
global $wpdb;
|
|
$term_posts = $wpdb->get_col($wpdb->prepare(
|
"SELECT object_id FROM {$wpdb->term_relationships} tr
|
JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
|
WHERE tt.term_id = %d AND tt.taxonomy = %s",
|
$term_id,
|
$taxonomy
|
));
|
|
$related_term_posts = $wpdb->get_col($wpdb->prepare(
|
"SELECT object_id FROM {$wpdb->term_relationships} tr
|
JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
|
WHERE tt.term_id = %d AND tt.taxonomy = %s",
|
$related_term_id,
|
$related_taxonomy
|
));
|
|
return count(array_intersect($term_posts, $related_term_posts));
|
}
|
|
// Get related terms based on a term ID and desired taxonomy
|
|
/**
|
* @param int $term_id
|
* @param string $desired_taxonomy
|
* @param bool $include_hierarchical
|
*
|
* @return array
|
*/
|
public function getRelatedTerms(int $term_id, string $desired_taxonomy, bool $include_hierarchical = true):array
|
{
|
|
$key = sprintf(
|
'%d_to_%s',
|
$term_id,
|
$desired_taxonomy
|
);
|
if ($include_hierarchical) {
|
$key.='_hierarchy';
|
}
|
$cache = $this->cache->get($key);
|
if ($cache) {
|
return $cache;
|
}
|
|
global $wpdb;
|
|
$term = get_term($term_id);
|
if (is_wp_error($term) || empty($term)) {
|
return [];
|
}
|
|
$query = $wpdb->prepare(
|
"SELECT related_term_id
|
FROM {$this->table_name}
|
WHERE term_id = %d
|
AND related_taxonomy = %s",
|
$term_id,
|
$desired_taxonomy
|
);
|
|
// Optionally include hierarchical relationships
|
if (!$include_hierarchical) {
|
$query .= " AND is_hierarchical = 0";
|
}
|
|
// Order by post count for popularity
|
$query .= " ORDER BY post_count DESC";
|
|
$related_term_ids = $wpdb->get_col($query);
|
|
$this->cache->set($key, $related_term_ids);
|
|
return $related_term_ids;
|
}
|
|
// Get all related terms based on multiple source terms (for "any" match)
|
|
/**
|
* @param array $term_ids
|
* @param string $taxonomy
|
*
|
* @return array
|
*/
|
public function getAnyRelatedTerms(array $term_ids, string $taxonomy):array
|
{
|
if (empty($term_ids)) {
|
return [];
|
}
|
|
$related_term_ids = [];
|
foreach ($term_ids as $term_id) {
|
$related = $this->getRelatedTerms($term_id, $taxonomy);
|
$related_term_ids = array_merge($related_term_ids, $related);
|
}
|
|
return array_unique($related_term_ids);
|
}
|
|
// Get related terms that are common to all source terms (for "all" match)
|
|
/**
|
* @param array $term_ids
|
* @param string $taxonomy
|
*
|
* @return array
|
*/
|
public function getAllRelatedTerms(array $term_ids, string $taxonomy):array
|
{
|
if (empty($term_ids)) {
|
return [];
|
}
|
|
$related_sets = [];
|
foreach ($term_ids as $term_id) {
|
$related = $this->getRelatedTerms($term_id, $taxonomy);
|
if (!empty($related)) {
|
$related_sets[] = $related;
|
}
|
}
|
|
if (count($related_sets) === 1) {
|
return $related_sets[0];
|
} elseif (count($related_sets) > 1) {
|
// Find intersection of all sets
|
$result = array_intersect(...$related_sets);
|
return array_values($result);
|
}
|
|
return [];
|
}
|
|
// Rebuild all relationships (useful for initial setup)
|
|
/**
|
* @return true
|
*/
|
public function rebuildAllRelationships():bool
|
{
|
$this->cache->flush();
|
global $wpdb;
|
|
// Clear existing relationships
|
$wpdb->query("TRUNCATE TABLE {$this->table_name}");
|
$total_posts = $wpdb->get_var("SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish'");
|
|
// Calculate number of batches needed (50 posts per batch)
|
$batch_size = 50;
|
$total_batches = ceil($total_posts / $batch_size);
|
|
// Queue the operation
|
$queue = JVB()->queue();
|
$operation_id = 'taxonomy_rebuild_' . uniqid();
|
|
$queue->queueOperation(
|
'taxonomy_relationships',
|
1,
|
[
|
'action' => 'rebuild_all',
|
'offset' => 0,
|
'limit' => $batch_size,
|
'total_posts' => $total_posts,
|
'total_batches' => $total_batches
|
],
|
[
|
'operation_id' => $operation_id,
|
'count' => $total_batches,
|
'priority' => 'normal',
|
]
|
);
|
|
$this->cache->flush();
|
|
return true;
|
}
|
|
/**
|
* @param WP_Error|array $result
|
* @param object $operation
|
* @param array $data
|
*
|
* @return WP_Error|array
|
*/
|
public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
|
{
|
if ($operation->type !== 'taxonomy_relationships') {
|
return $result;
|
}
|
|
if (isset($data['action']) && $data['action'] === 'rebuild_all') {
|
// Clear existing relationships
|
global $wpdb;
|
|
// Get batch of posts to process
|
$offset = isset($data['offset']) ? $data['offset'] : 0;
|
$limit = isset($data['limit']) ? $data['limit'] : 50; // Process in batches
|
|
$posts = $wpdb->get_col($wpdb->prepare(
|
"SELECT ID FROM {$wpdb->posts}
|
WHERE post_status = 'publish'
|
ORDER BY ID
|
LIMIT %d OFFSET %d",
|
$limit,
|
$offset
|
));
|
|
// Process each post
|
foreach ($posts as $post_id) {
|
$this->updatePostRelationships($post_id);
|
}
|
|
// Get total number of posts
|
$total_posts = $wpdb->get_var("SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish'");
|
|
// Return progress information
|
return [
|
'success' => true,
|
'result' => [
|
'processed' => count($posts),
|
'offset' => $offset,
|
'next_offset' => $offset + $limit,
|
'total' => $total_posts,
|
'completed' => ($offset + $limit >= $total_posts)
|
]
|
];
|
}
|
return [
|
'success' => false,
|
'message' => __('Hmmm.', 'jvb')
|
];
|
}
|
|
// Hook this to term deletion
|
|
/**
|
* @param int $term_id
|
*
|
* @return void
|
*/
|
public function deleteTermRelationships(int $term_id):void
|
{
|
global $wpdb;
|
|
$wpdb->query($wpdb->prepare(
|
"DELETE FROM {$this->table_name}
|
WHERE term_id = %d OR related_term_id = %d",
|
$term_id,
|
$term_id
|
));
|
}
|
}
|