| | |
| | | protected static string $termIDType; |
| | | protected static string $postIDType; |
| | | protected static string $postTable; |
| | | protected ?CustomTable $karmaTable = null; |
| | | protected ?CustomTable $favouritesTable = null; |
| | | |
| | | protected Cache $cache; |
| | | |
| | |
| | | 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(); |
| | |
| | | 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 |
| | |
| | | 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 |
| | | * |
| | |
| | | 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); |
| | |
| | | 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 = []; |
| | |
| | | |
| | | // 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}"; |
| | | } |
| | |
| | | ]; |
| | | } |
| | | |
| | | 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 |
| | |
| | | $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"]); |
| | |
| | | } |
| | | $args = ['where' => $where]; |
| | | if ($orderby) { |
| | | $args['orderby'] = $orderby; |
| | | $args['orderby'] = $this->resolveOrderby($orderby); |
| | | } |
| | | if ($order) { |
| | | $args['order'] = $order; |
| | |
| | | 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 = []; |
| | |
| | | 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); |
| | |
| | | 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, |
| | |
| | | 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); |
| | | } |
| | | } |