| | |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use wpdb; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | */ |
| | | class CustomTable |
| | | { |
| | | protected \wpdb $wpdb; |
| | | |
| | | private static ?wpdb $wpdb = null; |
| | | private static string $prefix; |
| | | protected string $tableName; |
| | | protected bool $table_exists = false; |
| | | protected string $definition; |
| | | protected string $fullTableName; |
| | | protected bool $useTransactions; |
| | |
| | | protected static string $termIDType; |
| | | protected static string $postIDType; |
| | | protected static string $postTable; |
| | | protected ?CustomTable $karmaTable = null; |
| | | protected ?CustomTable $favouritesTable = null; |
| | | |
| | | protected Cache $cache; |
| | | |
| | | /** |
| | | * Fluent factory method |
| | | * |
| | | * @param string $tableName Table name without prefix/BASE |
| | | * @param string $tableName |
| | | * @param bool $user |
| | | * @param bool $auth |
| | | * @param bool $useTransactions |
| | | * @return self |
| | | * |
| | | * @example CustomTable::for('favourites')->insert($data); |
| | | */ |
| | | public static function for(string $tableName): self |
| | | public static function for(string $tableName, bool $user = false, bool $auth = false, bool $useTransactions = false): self |
| | | { |
| | | if (!isset(self::$instances[$tableName])) { |
| | | self::$instances[$tableName] = new self($tableName); |
| | | self::$instances[$tableName] = new self($tableName, $user, $auth, $useTransactions); |
| | | self::$instances[$tableName]->setFlags(); |
| | | } |
| | | |
| | | |
| | | return self::$instances[$tableName]; |
| | | } |
| | | protected function setFlags():void |
| | | { |
| | | $this->table_exists = !is_null(self::$wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")); |
| | | } |
| | | |
| | | public static function destroyInstance(string $tableName):void |
| | | { |
| | | if (isset(self::$instances[$tableName])) { |
| | |
| | | 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(); |
| | |
| | | protected function ensureUserTable():void |
| | | { |
| | | if (!isset(static::$userTable)) { |
| | | static::$userTable = is_multisite() ? $this->getMultisiteUsersTable() : $this->wpdb->users; |
| | | static::$userTable = is_multisite() ? $this->getMultisiteUsersTable() : self::$wpdb->users; |
| | | } |
| | | } |
| | | protected function getMultisiteUsersTable():string |
| | | { |
| | | $siteUsersTable = $this->wpdb->prefix . 'users'; |
| | | $siteExists = $this->wpdb->get_var( |
| | | $this->wpdb->prepare("SHOW TABLES LIKE %s", $siteUsersTable) |
| | | $siteUsersTable = self::$wpdb->prefix . 'users'; |
| | | $siteExists = self::$wpdb->get_var( |
| | | self::$wpdb->prepare("SHOW TABLES LIKE %s", $siteUsersTable) |
| | | ); |
| | | if ($siteExists) { |
| | | return $siteUsersTable; |
| | | } |
| | | //fallback to main one |
| | | return $this->wpdb->users; |
| | | return self::$wpdb->users; |
| | | } |
| | | protected function ensureUserIDType():void |
| | | { |
| | |
| | | protected function ensureTermIDType():void |
| | | { |
| | | if (!isset(static::$termIDType)) { |
| | | static::$termIDType = $this->getColumnType($this->wpdb->terms, 'term_id'); |
| | | static::$termIDType = $this->getColumnType(self::$wpdb->terms, 'term_id'); |
| | | } |
| | | } |
| | | |
| | | protected function ensureTermTable():void |
| | | { |
| | | if (!isset(static::$termTable)) { |
| | | static::$termTable = $this->wpdb->terms; |
| | | static::$termTable = self::$wpdb->terms; |
| | | } |
| | | } |
| | | |
| | | protected function ensurePostIDType():void |
| | | { |
| | | if (!isset(static::$postIDType)) { |
| | | static::$postIDType = $this->getColumnType($this->wpdb->posts, 'ID'); |
| | | static::$postIDType = $this->getColumnType(self::$wpdb->posts, 'ID'); |
| | | } |
| | | } |
| | | protected function ensurePostTable():void |
| | | { |
| | | if (!isset(static::$postTable)) { |
| | | static::$postTable = $this->wpdb->posts; |
| | | static::$postTable = self::$wpdb->posts; |
| | | } |
| | | } |
| | | |
| | |
| | | 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 |
| | |
| | | require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); |
| | | |
| | | foreach (self::$instances as $instance) { |
| | | if (!$instance->wpdb->get_var("SHOW TABLES LIKE '{$instance->fullTableName}'")) { |
| | | if (!$instance->table_exists) { |
| | | $instance->createTable(); |
| | | } |
| | | } |
| | |
| | | error_log('[CustomTable]No definition set for '.$this->tableName); |
| | | return; |
| | | } |
| | | if ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) { |
| | | if ($this->table_exists) { |
| | | return; |
| | | } |
| | | require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); |
| | | error_log('Creating Database Table: '. $this->tableName); |
| | | $charset = self::$charsetCollate; |
| | | $schema = "CREATE TABLE IF NOT EXISTS {$this->fullTableName} {$this->definition} {$charset}"; |
| | | $this->wpdb->flush(); |
| | | self::$wpdb->flush(); |
| | | |
| | | $hasForeignKey = stripos($schema, 'FOREIGN KEY') !== false; |
| | | if ($hasForeignKey) { |
| | | $result = $this->wpdb->query($schema); |
| | | $success = ($result !== false && !$this->wpdb->last_error); |
| | | $result = self::$wpdb->query($schema); |
| | | $success = ($result !== false && !self::$wpdb->last_error); |
| | | } else { |
| | | dbDelta($schema.';'); |
| | | $success = !$this->wpdb->last_error; |
| | | $success = !self::$wpdb->last_error; |
| | | } |
| | | |
| | | if (!$success) { |
| | | $error_msg = "SQL Error creating table {$this->tableName}: " . $this->wpdb->last_error; |
| | | $error_msg = "SQL Error creating table {$this->tableName}: " . self::$wpdb->last_error; |
| | | error_log($error_msg); |
| | | error_log("Failed SQL Query: " . $schema); |
| | | } elseif ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) { |
| | | } else { |
| | | error_log("Successfully created table: {$this->fullTableName}"); |
| | | } |
| | | } |
| | |
| | | self::$instances = []; |
| | | } |
| | | |
| | | protected function checkWPDB():void |
| | | { |
| | | if (is_null(self::$wpdb)) { |
| | | global $wpdb; |
| | | self::$wpdb = $wpdb; |
| | | self::$prefix = $wpdb->prefix; |
| | | |
| | | $usersStatus = self::$wpdb->get_row("SHOW TABLE STATUS LIKE '{self::$wpdb->users}'"); |
| | | $parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci'; |
| | | self::$charsetCollate = "DEFAULT CHARACTER SET utf8mb4 COLLATE {$parentCollation}"; |
| | | } |
| | | |
| | | } |
| | | |
| | | /** |
| | | * @param string $tableName Table name without prefix/BASE (e.g., 'favourites', 'notifications') |
| | | * @param bool $useTransactions Whether to auto-wrap operations in transactions |
| | | */ |
| | | public function __construct(string $tableName, bool $useTransactions = false) |
| | | private function __construct(string $tableName, bool $user = false, bool $auth = false, bool $useTransactions = false) |
| | | { |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->checkWPDB(); |
| | | |
| | | $this->tableName = $tableName; |
| | | $this->fullTableName = $wpdb->prefix . apply_filters('jvb_base', BASE) . $tableName; |
| | | $this->fullTableName = self::$prefix . apply_filters('jvb_base', BASE) . $tableName; |
| | | $this->useTransactions = $useTransactions; |
| | | |
| | | $this->cache = Cache::for($tableName); |
| | | if ($user) { |
| | | $this->cache->user(); |
| | | } |
| | | if ($auth) { |
| | | $this->cache->auth(); |
| | | } |
| | | |
| | | $usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->users}'"); |
| | | $parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci'; |
| | | self::$charsetCollate = "DEFAULT CHARACTER SET utf8mb4 COLLATE {$parentCollation}"; |
| | | |
| | | } |
| | | |
| | |
| | | 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 LIMIT |
| | | * Set pagination |
| | | * |
| | | * @param int $limit Number of records |
| | | * @param int $offset Optional offset |
| | | * @param int $perPage Number of records per page |
| | | * @param int $page Page number (1-based) |
| | | * @return self |
| | | */ |
| | | public function limit(int $limit, int $offset = 0): self |
| | | public function limit(int $perPage, int $page = 1): self |
| | | { |
| | | $this->builder['limit'] = $limit; |
| | | $this->builder['offset'] = $offset; |
| | | $this->builder['per_page'] = $perPage; |
| | | $this->builder['page'] = $page; |
| | | return $this; |
| | | } |
| | | |
| | |
| | | $data['created_at'] = current_time('mysql'); |
| | | } |
| | | |
| | | $result = $this->wpdb->insert( |
| | | $result = self::$wpdb->insert( |
| | | $this->fullTableName, |
| | | $data, |
| | | $format |
| | |
| | | |
| | | $this->cache->flush(); |
| | | |
| | | return $this->wpdb->insert_id; |
| | | return self::$wpdb->insert_id; |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | /** |
| | | * Find or create a record |
| | | * |
| | | * @param array $searchData Data to search for |
| | | * @param array $createData Optional additional data for creation |
| | | * @return array ['id' => int, 'created' => bool, 'record' => object] |
| | | * @return int|false The ID of the created record, or false |
| | | * |
| | | * @example |
| | | * $result = CustomTable::for('favourites')->findOrCreate( |
| | | * ['user_id' => 1, 'target_id' => 123], |
| | | * ['type' => 'tattoo'] |
| | | * ); |
| | | * // Returns: ['id' => 456, 'created' => false, 'record' => object] |
| | | * $result = CustomTable::for('favourites')->findOrCreate( |
| | | * ['user_id' => 1, 'target_id' => 123], |
| | | * ['type' => 'tattoo'] |
| | | * ); |
| | | */ |
| | | public function findOrCreate(array $searchData, array $createData = []):int|false |
| | | { |
| | |
| | | (" . implode(',', $columns_escaped) . ") |
| | | VALUES " . implode(',', $placeholders); |
| | | |
| | | $result = $this->wpdb->query($this->wpdb->prepare($query, $values)); |
| | | $result = self::$wpdb->query(self::$wpdb->prepare($query, $values)); |
| | | |
| | | if ($result === false) { |
| | | $this->logError('bulkInsert', ['rows' => count($rows)]); |
| | |
| | | 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); |
| | | $query .= " WHERE {$where['sql']}"; |
| | | $values = $where['values']; |
| | | |
| | | return $this->wpdb->get_row($this->wpdb->prepare($query, $values), $output); |
| | | return self::$wpdb->get_row(self::$wpdb->prepare($query, $values), $output); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Get multiple records |
| | | * |
| | | * @param array $args Query arguments: where, orderby, order, limit, offset |
| | | * @param bool $itemsOnly |
| | | * @param string $output OBJECT, ARRAY_A, or ARRAY_N |
| | | * @return array |
| | | * |
| | | * @example |
| | | * $favs = $table->getMany([ |
| | | * 'where' => ['user_id' => 1], |
| | | * 'orderby' => 'date_added', |
| | | * 'order' => 'DESC', |
| | | * 'limit' => 20 |
| | | * ]); |
| | | * $favs = $table->getMany([ |
| | | * 'where' => ['user_id' => 1], |
| | | * 'orderby' => 'date_added', |
| | | * 'order' => 'DESC', |
| | | * 'per_page' => 20 |
| | | * ]); |
| | | */ |
| | | public function getMany(array $args = [], string $output = OBJECT): array |
| | | public function getMany(array $args = [], bool $itemsOnly = true, string $output = OBJECT): array |
| | | { |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey(array_merge($args, ['output' => $output])), |
| | | $items = $this->cache->remember( |
| | | 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}"; |
| | | } |
| | | |
| | | // LIMIT |
| | | if (!empty($args['limit'])) { |
| | | $limit = absint($args['limit']); |
| | | $offset = !empty($args['offset']) ? absint($args['offset']) : 0; |
| | | $query .= " LIMIT {$offset}, {$limit}"; |
| | | if (array_key_exists('limit', $args)) { |
| | | error_log('[CustomTable]::getMany deprecated key \'limit\' - use \'per_page\' instead. '.print_r($args, true)); |
| | | $args['per_page'] = $args['limit']; |
| | | } |
| | | if (array_key_exists('offset', $args)) { |
| | | error_log('[CustomTable]::getMany deprecated key \'offset\' - use \'page\' instead. '.print_r($args, true)); |
| | | $args['page'] = $args['offset'] / $args['limit']; |
| | | } |
| | | if (!empty($args['per_page'])) { |
| | | $perPage = absint($args['per_page']); |
| | | $page = !empty($args['page']) ? absint($args['page']) : 1; |
| | | $offset = ($page - 1) * $perPage; |
| | | $query .= " LIMIT {$offset}, {$perPage}"; |
| | | } |
| | | |
| | | if (empty($values)) { |
| | | return $this->wpdb->get_results($query, $output); |
| | | return self::$wpdb->get_results($query, $output); |
| | | } |
| | | |
| | | return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output); |
| | | return self::$wpdb->get_results(self::$wpdb->prepare($query, $values), $output); |
| | | } |
| | | ); |
| | | |
| | | if ($itemsOnly) { |
| | | return $items; |
| | | } |
| | | $page = max(1, $args['page'] ?? 1); |
| | | $perPage = $args['per_page']??false; |
| | | $total = $this->count($args['where']); |
| | | return [ |
| | | 'items' => $items, |
| | | 'total' => $total, |
| | | 'has_more' => $perPage && ($page * $perPage) < $total, |
| | | ]; |
| | | } |
| | | |
| | | 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 |
| | | * @param string $column |
| | | * @param array $where |
| | | * @param string|null $orderby |
| | | * @param string|null $order |
| | | * @param int|null $limit |
| | | * @return array |
| | | */ |
| | | public function pluck(string $column, array $where = [], ?string $orderby = null, ?string $order = null, ?int $limit = null): array |
| | |
| | | $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; |
| | | } |
| | | if ($limit) { |
| | | $args['limit'] = $limit; |
| | | $args['per_page'] = $limit; |
| | | } |
| | | |
| | | return array_column($this->getMany($args), $column); |
| | |
| | | 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 = []; |
| | |
| | | } |
| | | |
| | | if (empty($values)) { |
| | | return (int) $this->wpdb->get_var($query); |
| | | return (int) self::$wpdb->get_var($query); |
| | | } |
| | | |
| | | return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $values)); |
| | | return (int) self::$wpdb->get_var(self::$wpdb->prepare($query, $values)); |
| | | } |
| | | ); |
| | | |
| | |
| | | $data['updated_at'] = current_time('mysql'); |
| | | } |
| | | |
| | | $result = $this->wpdb->update( |
| | | $result = self::$wpdb->update( |
| | | $this->fullTableName, |
| | | $data, |
| | | $where, |
| | |
| | | */ |
| | | public function delete(array $where, ?array $where_format = null): int|false |
| | | { |
| | | $result = $this->wpdb->delete( |
| | | $result = self::$wpdb->delete( |
| | | $this->fullTableName, |
| | | $where, |
| | | $where_format |
| | |
| | | * |
| | | * @param string $query SQL query (use {table} as placeholder) |
| | | * @param array $values Values for prepare() |
| | | * @return mixed Query result |
| | | * @return int|bool|\mysqli_result|null Query result |
| | | * |
| | | * @example |
| | | * $results = $table->query( |
| | |
| | | * [1, 'pending', 'active'] |
| | | * ); |
| | | */ |
| | | public function query(string $query, array $values = []): mixed |
| | | public function query(string $query, array $values = []): int|bool|null|\mysqli_result |
| | | { |
| | | $query = str_replace('{table}', $this->fullTableName, $query); |
| | | |
| | | if (empty($values)) { |
| | | return $this->wpdb->query($query); |
| | | return self::$wpdb->query($query); |
| | | } |
| | | |
| | | return $this->wpdb->query($this->wpdb->prepare($query, $values)); |
| | | return self::$wpdb->query(self::$wpdb->prepare($query, $values)); |
| | | } |
| | | |
| | | /** |
| | |
| | | $query = str_replace('{table}', $this->fullTableName, $query); |
| | | |
| | | if (empty($values)) { |
| | | return $this->wpdb->get_results($query, $output); |
| | | return self::$wpdb->get_results($query, $output); |
| | | } |
| | | |
| | | return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output); |
| | | return self::$wpdb->get_results(self::$wpdb->prepare($query, $values), $output); |
| | | } |
| | | |
| | | /** |
| | |
| | | * |
| | | * @param string $query SQL query (use {table} as placeholder) |
| | | * @param array $values Values for prepare() |
| | | * @return mixed |
| | | * @return string|null |
| | | */ |
| | | public function queryVar(string $query, array $values = []): mixed |
| | | public function queryVar(string $query, array $values = []): ?string |
| | | { |
| | | $query = str_replace('{table}', $this->fullTableName, $query); |
| | | |
| | | if (empty($values)) { |
| | | return $this->wpdb->get_var($query); |
| | | return self::$wpdb->get_var($query); |
| | | } |
| | | |
| | | return $this->wpdb->get_var($this->wpdb->prepare($query, $values)); |
| | | return self::$wpdb->get_var(self::$wpdb->prepare($query, $values)); |
| | | } |
| | | |
| | | // ========================================================================= |
| | |
| | | */ |
| | | public function startTransaction(): void |
| | | { |
| | | $this->wpdb->query('START TRANSACTION'); |
| | | self::$wpdb->query('START TRANSACTION'); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function commit(): void |
| | | { |
| | | $this->wpdb->query('COMMIT'); |
| | | self::$wpdb->query('COMMIT'); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function rollback(): void |
| | | { |
| | | $this->wpdb->query('ROLLBACK'); |
| | | self::$wpdb->query('ROLLBACK'); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function getInsertId(): int |
| | | { |
| | | return $this->wpdb->insert_id; |
| | | return self::$wpdb->insert_id; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function getLastError(): string |
| | | { |
| | | return $this->wpdb->last_error; |
| | | return self::$wpdb->last_error; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function getAffectedRows(): int |
| | | { |
| | | return $this->wpdb->rows_affected; |
| | | return self::$wpdb->rows_affected; |
| | | } |
| | | |
| | | public function getUserIDType():string |
| | |
| | | // PRIVATE HELPERS |
| | | // ========================================================================= |
| | | private function getColumnType(string $table, string $column):string|false { |
| | | $tableExists = $this->wpdb->get_var( |
| | | $this->wpdb->prepare("SHOW TABLES LIKE %s", $table) |
| | | $tableExists = self::$wpdb->get_var( |
| | | self::$wpdb->prepare("SHOW TABLES LIKE %s", $table) |
| | | ); |
| | | if (!$tableExists) { |
| | | error_log("[CustomTable] Table {$table} does not exist for getColumnType"); |
| | | return 'bigint(20) unsigned'; // safe fallback |
| | | } |
| | | |
| | | $result = $this->wpdb->get_row( |
| | | $this->wpdb->prepare( |
| | | $result = self::$wpdb->get_row( |
| | | self::$wpdb->prepare( |
| | | "SELECT COLUMN_TYPE |
| | | FROM INFORMATION_SCHEMA.COLUMNS |
| | | WHERE TABLE_SCHEMA = DATABASE() |
| | |
| | | |
| | | private function tableExists():bool |
| | | { |
| | | return !is_null($this->wpdb->get_var($this->wpdb->prepare("SHOW TABLES LIKE %s", $this->fullTableName))); |
| | | return !is_null(self::$wpdb->get_var(self::$wpdb->prepare("SHOW TABLES LIKE %s", $this->fullTableName))); |
| | | } |
| | | /** |
| | | * Build WHERE clause from associative array |
| | |
| | | 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); |
| | |
| | | static $cache = []; |
| | | |
| | | if (!isset($cache[$this->tableName])) { |
| | | $columns = $this->wpdb->get_col("DESCRIBE {$this->fullTableName}"); |
| | | $columns = self::$wpdb->get_col("DESCRIBE {$this->fullTableName}"); |
| | | $cache[$this->tableName] = array_flip($columns); |
| | | } |
| | | |
| | |
| | | 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' |
| | | "CustomTable {$operation} failed: " . self::$wpdb->last_error, |
| | | $context |
| | | ); |
| | | } else { |
| | | error_log("[CustomTable:{$this->tableName}] {$operation} failed: " . $this->wpdb->last_error); |
| | | error_log("[CustomTable:{$this->tableName}] {$operation} failed: " . self::$wpdb->last_error); |
| | | } |
| | | } |
| | | |
| | | public function sum(string $column, array $where):float |
| | | { |
| | | $rows = $this->pluck($column, $where); |
| | | return array_sum($rows); |
| | | } |
| | | } |