Jake Vanderwerf
2026-08-26 ccf5e61a075c35d24befc175994b5cb8957ca322
inc/managers/CustomTable.php
@@ -38,6 +38,8 @@
   protected static string $termIDType;
   protected static string $postIDType;
   protected static string $postTable;
   protected ?CustomTable $karmaTable = null;
   protected ?CustomTable $favouritesTable = null;
   protected Cache $cache;
@@ -73,6 +75,15 @@
      return self::$instances[$tableName];
   }
   public function setKarma(CustomTable $karma):void
   {
      $this->karmaTable = $karma;
   }
   public function setFavourite(CustomTable $favourites):void
   {
      $this->favouritesTable = $favourites;
   }
   public function ensureDefined() {
      $this->ensureUserTable();
      $this->ensureUserIDType();
@@ -143,6 +154,11 @@
      return $this;
   }
   public function getColumnKeys():array
   {
      return array_keys($this->columns);
   }
   /**
    * @param array $keys An array of {string} $keys. If a $key is an array, you can define a custom $key => $value, example: 'UNIQUE' => $value, or 'PRIMARY' => $value
    * @return $this
@@ -318,6 +334,26 @@
      return $this;
   }
   public function orderByKarma(string $metric = 'karmic_score', string $direction = 'DESC'): self
   {
      if (!$this->karmaTable || !in_array($metric, ['upvotes', 'downvotes', 'karmic_score'])) {
         return $this->orderBy('created_at');
      }
      $this->builder['orderby'] = $metric;
      $this->builder['order'] = strtoupper($direction);
      return $this;
   }
   public function orderByFavourites(string $direction = 'DESC'): self
   {
      if (!$this->favouritesTable) {
         return $this->orderBy('created_at');
      }
      $this->builder['orderby'] = 'favourites_count';
      $this->builder['order'] = strtoupper($direction);
      return $this;
   }
   /**
    * Set pagination
    *
@@ -577,7 +613,7 @@
   public function get(array $where, string $output = OBJECT): object|array|null
   {
      return $this->cache->remember(
         $this->cache->generateKey(array_merge(['output' => $output], $where)),
         array_merge(['output' => $output], $where),
         function() use ($where, $output) {
            $query = "SELECT * FROM {$this->fullTableName}";
            $where = $this->buildWhereClause($where);
@@ -607,7 +643,7 @@
   public function getMany(array $args = [], bool $itemsOnly = true, string $output = OBJECT): array
   {
      $items = $this->cache->remember(
         $this->cache->generateKey(array_merge($args, ['output' => $output])),
         array_merge($args, ['output' => $output]),
         function () use ($args, $output) {
            $query = "SELECT * FROM {$this->fullTableName}";
            $values = [];
@@ -622,7 +658,7 @@
            // ORDER BY
            if (!empty($args['orderby'])) {
               $orderby = sanitize_sql_orderby($args['orderby']);
               $orderby = $this->resolveOrderby($args['orderby']);
               $order = (!empty($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC';
               $query .= " ORDER BY {$orderby} {$order}";
            }
@@ -664,6 +700,26 @@
      ];
   }
   protected function resolveOrderby(string $orderby):string
   {
      $orderby = strtolower($orderby);
      if (in_array($orderby, ['upvotes', 'downvotes', 'karmic_score']) && $this->karmaTable) {
         $tableName = $this->karmaTable->getFullTableName();
         return match ($orderby) {
            'upvotes'   => "(SELECT COUNT(*) FROM {$tableName} k WHERE k.item_id = {$this->fullTableName}.id AND k.vote = 'up')",
            'downvotes' => "(SELECT COUNT(*) FROM {$tableName} k WHERE k.item_id = {$this->fullTableName}.id AND k.vote = 'down')",
            'karmic_score'     => "(SELECT COALESCE(SUM(CASE WHEN k.vote = 'up' THEN 1 WHEN k.vote = 'down' THEN -1 ELSE 0 END), 0) FROM {$tableName} k WHERE k.item_id = {$this->fullTableName}.id)",
         };
      }
      if ($orderby === 'favourites_count' && $this->favouritesTable) {
         $tableName = $this->favouritesTable->getFullTableName();
         return "(SELECT COUNT(*) FROM {$tableName} f WHERE f.target_id = {$this->fullTableName}.id)";
      }
      return sanitize_sql_orderby($orderby);
   }
   /**
    * Get a specific column value from all matches
@@ -676,7 +732,7 @@
      $key = array_merge($where, ['column' => $column, 'orderby' => $orderby, 'order' => $order, 'limit' => $limit]);
      return $this->cache->remember(
         $this->cache->generateKey($key),
         $key,
         function() use ($column, $where, $orderby, $order, $limit) {
            if (!empty($this->columns) && !array_key_exists($column, $this->columns)) {
               $this->logError('pluck', ['reason' => "Column '{$column}' not in definition"]);
@@ -684,7 +740,7 @@
            }
            $args = ['where' => $where];
            if ($orderby) {
               $args['orderby'] = $orderby;
               $args['orderby'] = $this->resolveOrderby($orderby);
            }
            if ($order) {
               $args['order'] = $order;
@@ -708,7 +764,7 @@
   public function count(array $where = []): int
   {
      return $this->cache->remember(
         $this->cache->generateKey(array_merge(['source' => 'count'], $where)),
         array_merge(['source' => 'count'], $where),
         function() use ($where) {
            $query = "SELECT COUNT(*) FROM {$this->fullTableName}";
            $values = [];
@@ -1045,16 +1101,34 @@
      foreach ($where as $column => $value) {
         $column_safe = esc_sql($column);
         // OR grouping: ['OR' => [ [...cond1], [...cond2] ]]
         if ($column === 'OR' && is_array($value)) {
            $orConditions = [];
            foreach ($value as $orWhere) {
               $orClause = $this->buildWhereClause($orWhere);
               $orConditions[] = $orClause['sql'];
               $values = array_merge($values, $orClause['values']);
            }
            $conditions[] = '(' . implode(' OR ', $orConditions) . ')';
            continue;
         }
         if ($value === null) {
            $conditions[] = "`{$column_safe}` IS NULL";
         } elseif (is_array($value) && count($value) === 2 && is_string($value[0])) {
            [$operator, $operand] = $value;
            $operator = strtoupper(trim($operator));
            if (in_array($operator, ['IN', 'NOT IN']) && is_array($operand)) {
            if ($operator === 'IS NOT NULL') {
               $conditions[] = "`{$column_safe}` IS NOT NULL";
            } elseif (in_array($operator, ['IN', 'NOT IN']) && is_array($operand)) {
               $placeholders = implode(',', array_map([$this, 'getPlaceholder'], $operand));
               $conditions[] = "`{$column_safe}` {$operator} ({$placeholders})";
               $values       = array_merge($values, $operand);
            } elseif (in_array($operator, ['BETWEEN', 'NOT BETWEEN']) && is_array($operand) && count($operand) === 2) {
               $conditions[] = "`{$column_safe}` {$operator} " . $this->getPlaceholder($operand[0]) . " AND " . $this->getPlaceholder($operand[1]);
               $values[]     = $operand[0];
               $values[]     = $operand[1];
            } else {
               // <, >, <=, >=, !=, LIKE, etc.
               $conditions[] = "`{$column_safe}` {$operator} " . $this->getPlaceholder($operand);
@@ -1104,7 +1178,7 @@
   private function logError(string $operation, array $context = []): void
   {
      if (function_exists('JVB')) {
         JVB()->error()->log(
         Errors::getInstance()->log(
            $this->tableName,
            "CustomTable {$operation} failed: " . $this->wpdb->last_error,
            $context,
@@ -1114,4 +1188,10 @@
         error_log("[CustomTable:{$this->tableName}] {$operation} failed: " . $this->wpdb->last_error);
      }
   }
   public function sum(string $column, array $where):float
   {
      $rows = $this->pluck($column, $where);
      return array_sum($rows);
   }
}