<?php
|
namespace JVBase\managers;
|
|
use Exception;
|
use WP_Error;
|
use WP_Post;
|
use WP_Query;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
class TaxonomyRelationships
|
{
|
protected Cache $cache;
|
protected CustomTable $relationships;
|
|
public function __construct()
|
{
|
$this->defineTables();
|
|
$this->cache = Cache::for('term_relationship', WEEK_IN_SECONDS);
|
$this->cache->connect('terms');
|
|
// Ensure the table exists
|
// $this->create_table_if_not_exists();
|
|
|
add_action('init', [$this, 'init']);
|
}
|
|
protected function defineTables():void
|
{
|
$relationships = CustomTable::for('taxonomy_relationships');
|
|
$relationships->setColumns([
|
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
|
'term_id' => "{$relationships->getTermIDType()} NOT NULL",
|
'related_term_id' => "{$relationships->getTermIDType()} NOT NULL",
|
'taxonomy' => 'varchar(32) NOT NULL',
|
'related_taxonomy' => 'varchar(32) NOT NULL',
|
'post_count' => 'int(11) NOT NULL DEFAULT 0',
|
'is_direct' => 'tinyint(1) NOT NULL DEFAULT 1',
|
'is_hierarchical' => 'tinyint(1) NOT NULL DEFAULT 0',
|
'last_updated' => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
|
]);
|
|
$relationships->setKeys([
|
['key'=>'PRIMARY', 'value' => '(`id`)'],
|
['key'=>'UNIQUE', 'value' => '`relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`)'],
|
'`term_id` (`term_id`)',
|
'`related_term_id` (`related_term_id`)',
|
'`taxonomy` (`taxonomy`)',
|
'`related_taxonomy` (`related_taxonomy`)'
|
]);
|
$base = BASE;
|
$relationships->setConstraints([
|
"CONSTRAINT {$base}tax_rel_term_id FOREIGN KEY (`term_id`)
|
REFERENCES `{$relationships->getTermTable()}` (`term_id`) ON DELETE CASCADE",
|
"CONSTRAINT `{$base}tax_rel_related_id` FOREIGN KEY (`related_term_id`)
|
REFERENCES `{$relationships->getTermTable()}` (`term_id`) ON DELETE CASCADE"
|
]);
|
$relationships->defineTable();
|
|
$this->relationships = $relationships;
|
}
|
|
/**
|
* 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
|
{
|
// Skip autosaves and revisions
|
if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
|
return;
|
}
|
$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
|
{
|
// 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;
|
}
|
|
// 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;
|
}
|
}
|
// Calculate number of shared posts
|
$shared_posts_count = $this->countSharedPosts($term_id, $related_term_id, $taxonomy, $related_taxonomy);
|
$updated = $this->relationships->findOrCreate([
|
'term_id' => $term_id,
|
'related_term_id' => $related_term_id,
|
'taxonomy' => $taxonomy,
|
'related_taxonomy' => $related_taxonomy
|
],[
|
'is_hierarchical' => $is_hierarchical,
|
'post_count' => $shared_posts_count
|
]);
|
|
return (bool)$updated;
|
}
|
|
/**
|
* @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
|
{
|
|
$term_posts = $this->cache->remember(
|
$this->cache->generateKey(['term' => $term_id, 'taxonomy' => $taxonomy]),
|
function () use ($term_id, $taxonomy) {
|
global $wpdb;
|
return $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 = $this->cache->remember(
|
$this->cache->generateKey(['term' => $related_term_id, 'taxonomy' => $related_taxonomy]),
|
function () use($related_term_id, $related_taxonomy) {
|
global $wpdb;
|
return $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
|
{
|
|
$where = [
|
'term_id' => $term_id,
|
'related_taxonomy' => $desired_taxonomy
|
];
|
if (!$include_hierarchical) {
|
$where['is_hierarchical'] = 0;
|
}
|
return $this->relationships->pluck('related_term_id', $where, 'post_count', 'DESC');
|
}
|
|
// 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
|
{
|
$term_ids = array_filter(array_map('absint', $term_ids));
|
return array_unique($this->relationships->pluck('related_term_id', ['term_id' => ['IN' => $term_ids], 'related_taxonomy' => $taxonomy], 'post_count', 'DESC'));
|
|
}
|
|
// 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->relationships->getFullTableName()}");
|
|
$posts = new WP_Query([
|
'post_status' => 'publish',
|
'posts_per_page'=> -1,
|
'fields' => 'ids'
|
]);
|
if (!$posts->have_posts()) {
|
wp_reset_postdata();
|
return true;
|
}
|
|
$ids = $posts->posts;
|
wp_reset_postdata();
|
|
|
// Queue the operation
|
$queue = JVB()->queue();
|
|
$queue->queueOperation(
|
'taxonomy_relationships',
|
1,
|
[
|
'posts' => $ids,
|
],
|
[
|
'chunk_key' => 'posts',
|
'chunk_size' => 50
|
]
|
);
|
|
$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;
|
}
|
|
try {
|
foreach ($data['posts'] as $postID) {
|
$post = get_post($postID);
|
$this->updatePostRelationships($postID, $post);
|
}
|
} catch (Exception $e) {
|
return [
|
'success' => false,
|
'message' => $e->getMessage()
|
];
|
}
|
return [
|
'success' => true
|
];
|
}
|
|
// Hook this to term deletion
|
|
/**
|
* @param int $term_id
|
*
|
* @return void
|
*/
|
public function deleteTermRelationships(int $term_id):void
|
{
|
$termIDs = $this->relationships->delete(['term_id' => $term_id]);
|
$related = $this->relationships->delete(['related_term_id' => $term_id]);
|
}
|
}
|