Jake Vanderwerf
2026-08-26 18e7e415339ccc9adf8ade562f93abef606da16d
inc/rest/routes/TermRoutes.php
@@ -1,12 +1,15 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\Rest;
use JVBase\Instance;
use JVBase\managers\Cache;
use JVBase\managers\Errors;
use JVBase\managers\TaxonomyRelationships;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\managers\UserTermsManager;
use JVBase\rest\Route;
use JVBase\utility\Features;
use JVBase\base\Site;
use WP_REST_Request;
use WP_REST_Response;
use Exception;
@@ -16,6 +19,7 @@
}
class TermRoutes extends Rest
{
   use Instance;
    protected object $term_index_manager;
    protected int $per_page;
@@ -23,14 +27,12 @@
    {
        $this->cacheName = 'terms';
        parent::__construct();
      if (JVB_TESTING) {
         $this->cache->flush();
      }
      $this->cache->connect('taxonomy', true);
      $this->cache = Cache::for('terms', 'term');
        $this->per_page = 20;
        add_action('edited_term', [$this, 'deleteTermPath']);
        add_action('wp_login', [$this, 'clearUserTaxonomyCache'], 10, 2);
    }
    /**
@@ -56,12 +58,14 @@
            'taxonomy'  => 'string|required',
            'name'      => 'string|required',
            'parent' => 'int|default:0',
         ]);
         ])
         ->register();
      Route::for('terms/check')
         ->get([$this,'getTermDetails'])
         ->auth('public')
         ->rateLimit();
         ->rateLimit()
         ->register();
    }
    /**
@@ -87,7 +91,7 @@
        // Add relationship data if requested
        if ($request->get_param('include_relationships')) {
            $relationship_manager = new TaxonomyRelationships();
            $relationship_manager = TaxonomyRelationships::getInstance();
            $related_taxonomies = $request->get_param('related_taxonomies') ?: ['jvb_style', 'jvb_theme', 'jvb_city'];
            $data['relationships'] = [];
@@ -121,15 +125,9 @@
   public function getTermDetails(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_params();
      $taxonomies = array_keys($data);
      $cache_check = $this->checkHeaders($request, $this->cache->generateKey(['termDetails' => $data]));
      if ($cache_check) {
         return $cache_check;
      }
      $terms = $this->cache->remember(
         $this->cache->generateKey(['termDetails' => $data]),
         ['termDetails' => $data],
         function() use ($data) {
            $result = [];
            foreach ($data as $tax => $IDs) {
@@ -143,7 +141,7 @@
         }
      );
      return $this->addCacheHeaders($this->success(['items' => $terms]));
      return $this->success(['items' => $terms]);
   }
    /**
@@ -155,11 +153,8 @@
    {
      $data = $request->get_params();
      $taxonomy = sanitize_text_field($data['taxonomy'])??'';
      // Check HTTP cache headers
      $cache_check = $this->checkHeaders($request, $taxonomy);
      if ($cache_check) {
         error_log('Header Check failed');
         return $cache_check;
      if (empty($taxonomy)) {
         return $this->success(['message' => 'No taxonomy selected']);
      }
      // Handle batch request (multiple taxonomies)
@@ -174,41 +169,40 @@
            'include'   => $data['termIDs'],
            'hide_empty'   => true,
         ];
         $key = $this->cache->generateKey($args);
         $cached = $this->cache->get($key);
         if ($cached) {
            $response = $this->success($cached);
            return $this->addCacheHeaders($response);
         }
         $data = $this->cache->remember(
            $args,
            function() use ($args, $taxonomy) {
               return $this->formatTerms($args, $taxonomy);
            }
         );
         $formatted = $this->formatTerms($args, $taxonomy);
         $response = [
            'items'  => $formatted
            'items'  => $data
         ];
         $this->cache->set($key, $response);
         $response = $this->success($response);
         return $this->addCacheHeaders($response);
         return $this->success($response);
      }
      if (array_key_exists('content', $data)) {
         // If content_type is provided, use the specialized endpoint
         $content_type = $request->get_param('content');
         global $feed_types;
         if (taxIsJVBContentTax($content_type)) {
            $response = $this->getTermsForContentType($request);
            return $this->addCacheHeaders($response);
         $registrar = Registrar::getInstance($content_type);
         if ($registrar->hasFeature('is_content')) {
            //TODO: Not sure what we're trying for here. Doesn't seem to be used.
            //TODO: Likely for taxonomy content types, like tattoo shops
            return $this->emptyResult();
         }
      }
        $taxonomy = BASE.$request->get_param('taxonomy');
        $taxonomy = jvbCheckBase($request->get_param('taxonomy'));
      if (!taxonomy_exists($taxonomy)) {
         return $this->emptyResult();
      }
        $search = $request->get_param('search');
      $parent = (int)$data['parent']??0;
      $page = max(1, (int)($data['page']??1));
      $per_page = 25;
        if (!taxonomy_exists($taxonomy)) {
         return $this->emptyResult();
        }
        $tax_obj = get_taxonomy($taxonomy);
        $is_hierarchical = $tax_obj->hierarchical;
@@ -216,8 +210,8 @@
        // If searching, handle differently
        if (!empty($search)) {
         error_log('Handling search...');
            $response = $this->handleTermSearch($request);
         return $this->addCacheHeaders($response);
         return $this->handleTermSearch($request);
        }
        // Get terms for current level with child count
@@ -235,13 +229,12 @@
        if ($request->get_param('main_context') && in_array(jvbCheckBase(json_decode($request->get_param('main_context'), true)['context']), jvbUserTypes())) {
            $main_context = json_decode($request->get_param('main_context'), true);
            $userID = get_post_meta($main_context['id'], BASE.'link', true);
            $manager = new UserTermsManager();
            $related = $manager->getUserTermIDs($userID, $taxonomy);
            $userID = get_post_meta($main_context['id'], BASE.'profile_link', true);
            $manager = UserTermsManager::getInstance();
            $related = $manager->fetchUserTerms($userID, $taxonomy);
            if (empty($related)) {
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            return $this->emptyResult();
            }
            $args['include'] = $related;
@@ -250,12 +243,11 @@
            $main_context = json_decode($request->get_param('main_context'), true);
            $thisTaxonomy = str_replace('taxonomy:', '', $main_context['context']);
            $ID = (int)$main_context['id'];
            $manager = new TaxonomyRelationships();
            $manager = TaxonomyRelationships::getInstance();
            $related = $manager->getRelatedTerms($ID, BASE.$request->get_param('taxonomy'));
            if (empty($related)) {
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            return $this->emptyResult();
            }
            $args['tax_query'] = [
                'taxonomy'  => $taxonomy,
@@ -268,7 +260,7 @@
            $match = $request->get_param('match') ?? 'any';
            $context = json_decode($request['context'], true);
            $relationshipManager = new TaxonomyRelationships();
            $relationshipManager = TaxonomyRelationships::getInstance();
            // Prepare array to collect term IDs that match the context
            $related_term_ids = [];
@@ -304,49 +296,40 @@
                $args['include'] = $related_term_ids;
            } else {
                // No related terms found, return empty result
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            return $this->emptyResult();
            }
        }
      $response = $this->cache->remember(
         $args,
         function() use ($args, $taxonomy, $parent, $per_page, $page, $is_hierarchical) {
            $formatted_terms = $this->formatTerms($args, $taxonomy);
            // Get total count for pagination
            $total_terms = wp_count_terms([
               'taxonomy' => $taxonomy,
               'hide_empty' => false,
               'parent' => $parent
            ]);
            // Calculate pagination
            $total_pages = ceil($total_terms / $per_page);
            $has_more = $page < $total_pages;
        $key = $this->cache->generateKey($args);
        $cache = $this->cache->get($key);
            return [
               'items' => $formatted_terms,
               'is_hierarchical' => $is_hierarchical,
               'pagination' => [
                  'page' => $page,
                  'per_page' => $per_page,
                  'total_pages' => $total_pages,
                  'total_terms' => (int)$total_terms
               ],
               'has_more' => $has_more
            ];
         }
      );
        if ($cache) {
            $response = $this->success($cache);
         return $this->addCacheHeaders($response);
        }
        $formatted_terms = $this->formatTerms($args, $taxonomy);
        // Get total count for pagination
        $total_terms = wp_count_terms([
            'taxonomy' => $taxonomy,
            'hide_empty' => false,
            'parent' => $parent
        ]);
        // Calculate pagination
        $total_pages = ceil($total_terms / $per_page);
        $has_more = $page < $total_pages;
        $response = [
            'items' => $formatted_terms,
            'is_hierarchical' => $is_hierarchical,
            'pagination' => [
                'page' => $page,
                'per_page' => $per_page,
                'total_pages' => $total_pages,
                'total_terms' => (int)$total_terms
            ],
         'has_more' => $has_more
        ];
        $this->cache->set($key, $response);
        $response = $this->success($response);
      return $this->addCacheHeaders($response);
      return $this->success($response);
    }
   protected function handleBatchTermRequest(string $taxonomy, array $data, WP_REST_Request $request):WP_REST_Response
@@ -383,8 +366,7 @@
         'has_more' => true,
      ];
      $response = $this->success($response);
      return $this->addCacheHeaders($response);
      return $this->success($response);
   }
@@ -397,7 +379,7 @@
   protected function formatTerms(array $args, string $taxonomy): array
   {
      return $this->cache->remember(
         $this->cache->generateKey($args),
         $args,
         function() use ($args, $taxonomy) {
            $terms = get_terms($args);
@@ -407,7 +389,7 @@
            $formatted_terms = [];
            foreach ($terms as $term) {
               $formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, true);
               $formatted_terms[] = $this->formatSingleTerm($term, $taxonomy);
            }
            return $formatted_terms;
@@ -477,7 +459,7 @@
      ];
      $data = $this->cache->remember(
         $this->cache->generateKey($args),
         $args,
         function() use ($args, $taxonomy, $page, $per_page) {
            $terms = get_terms($args);
@@ -507,7 +489,7 @@
         }
      );
      return $this->addCacheHeaders($this->success($data));
      return $this->success($data);
   }
    /**
@@ -549,167 +531,6 @@
        delete_term_meta($termID, BASE.'term_path');
    }
    /**
     * @param string $username
     * @param object $user
     *
     * @return void
     */
    public function clearUserTaxonomyCache(string $username, object $user):void
    {
        global $wpdb;
        // Get all transients for this user
        $like_pattern = $wpdb->esc_like(BASE.'user_' . $user->ID) . '_%';
        $transients = $wpdb->get_col($wpdb->prepare("
            SELECT option_name
            FROM {$wpdb->options}
            WHERE option_name LIKE %s
            AND option_name LIKE %s
        ", '_transient_' . $like_pattern, '%'));
        // Delete all found transients
        foreach ($transients as $transient) {
            delete_transient(str_replace('_transient_', '', $transient));
        }
    }
    /**
     * @param WP_REST_Request $request
     *
     * @return WP_REST_Response
     */
    public function getTermsForContentType(WP_REST_Request $request):WP_REST_Response
    {
        $manager = new TaxonomyRelationships();
        $content_type = BASE . $request->get_param('content');
        $taxonomy = BASE . $request->get_param('taxonomy');
        $search = $request->get_param('search');
        $page = max(1, intval($request->get_param('page')));
        $per_page = $request->get_param('per_page') ?: 20;
        // Create cache key
        $cache_key = "terms_for_{$content_type}_{$taxonomy}_" . md5("{$search}_{$page}_{$per_page}");
        $cache_key = $this->cache->generateKey([
            'terms_for'     => $content_type,
            'taxonomy'      => $taxonomy,
            'search'        => $search,
            'page'          => $page,
            'per_page'      => $per_page
        ]);
        $cache = $this->cache->get($cache_key);
        // Try cache first
        if ($cache !== false) {
         $response = $this->success($cache);
            return $this->addCacheHeaders($response);
        }
        try {
            global $wpdb;
            // Starting the query building
            $query_args = [];
            $terms_query = "
                SELECT DISTINCT t.term_id, t.name, t.slug, tt.count, tt.taxonomy,
                       (SELECT COUNT(DISTINCT tm.meta_key) FROM {$wpdb->termmeta} tm WHERE tm.term_id = t.term_id) AS meta_count
                FROM {$wpdb->terms} t
                JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                JOIN {$manager->getTableName()} tr ON (
                    (tr.primary_taxonomy = %s AND tr.primary_term_id = t.term_id)
                    OR (tr.related_taxonomy = %s AND tr.related_term_id = t.term_id)
                )
            ";
            $query_args[] = $taxonomy;
            $query_args[] = $taxonomy;
            // Add content type filter through posts
            $query_where = " WHERE tt.taxonomy = %s";
            $query_args[] = $taxonomy;
            // Add search condition if provided
            if ($search) {
                $query_where .= " AND (t.name LIKE %s OR t.slug LIKE %s)";
                $search_like = '%' . $wpdb->esc_like($search) . '%';
                $query_args[] = $search_like;
                $query_args[] = $search_like;
            }
            // Add relationship strength ordering
            $query_order = " GROUP BY t.term_id";
            $query_order .= " ORDER BY COUNT(tr.id) DESC, tt.count DESC";
            // Add pagination
            $query_limit = " LIMIT %d OFFSET %d";
            $query_args[] = $per_page;
            $query_args[] = ($page - 1) * $per_page;
            // Combine the query
            $final_query = $query_where . $query_order . $query_limit;
            // Get terms
            $terms = $wpdb->get_results(
                $wpdb->prepare(
                    $terms_query . $final_query,
                    $query_args
                )
            );
            // Get total count for pagination
            $count_query = str_replace($query_limit, '', $query_where);
            $count_query = "SELECT COUNT(DISTINCT t.term_id) FROM {$wpdb->terms} t
                           JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                           JOIN {$manager->getTableName()} tr ON (
                               (tr.primary_taxonomy = %s AND tr.primary_term_id = t.term_id)
                               OR (tr.related_taxonomy = %s AND tr.related_term_id = t.term_id)
                           ) " . $count_query;
            // Remove pagination args
            array_pop($query_args);
            array_pop($query_args);
            $total = $wpdb->get_var(
                $wpdb->prepare(
                    $count_query,
                    $query_args
                )
            );
            // Format terms
            $formatted_terms = [];
            $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
         foreach ($terms as $term) {
            $formatted = $this->formatSingleTerm($term, $taxonomy, false);
            // Add relationship strength which is unique to this method
            $formatted['relationship_strength'] = $term->relationship_count ?? 0;
            $formatted_terms[] = $formatted;
         }
            // Build response
            $total_pages = ceil($total / $per_page);
            $results = [
                'items' => $formatted_terms,
                'pagination'    => [
                    'page'      => (int)$page,
                    'per_page'  => (int)$per_page,
                    'total_terms'=> $total,
                    'total_pages'=> $total_pages
                ],
            'has_more'  => $page < $total_pages
            ];
            // Cache results
            $this->cache->set($cache_key, $results);
         $response = $this->success($results);
            return $this->addCacheHeaders($response);
        } catch (Exception $e) {
         return $this->error('Error getting terms for content: '.$e->getMessage(), 'get_terms_for_content');
        }
    }
    /**
     * @param WP_REST_Request $request
@@ -735,8 +556,9 @@
            ]]);
         }
         if (Features::forMembership()->has('term_approval')) {
            $approval_routes = JVB()->routes('approvals');
         $membership = Site::membership();
         if ($membership && $membership->has('term_approval')) {
            $approval_routes = ApprovalRoutes::getInstance();
            $request_id = $approval_routes->createTermApprovalRequest(
               $user_id,
               $taxonomy,
@@ -779,7 +601,7 @@
         ], 201); // 201 Created
      } catch (Exception $e) {
         JVB()->error()->log(
         Errors::getInstance()->log(
            'terms',
            'Term creation failed: ' . $e->getMessage(),
            ['user_id' => $user_id, 'taxonomy' => $taxonomy, 'name' => $name]