| | |
| | | { |
| | | protected \wpdb $wpdb; |
| | | protected string $tableName; |
| | | protected string $definition; |
| | | protected string $fullTableName; |
| | | protected bool $useTransactions; |
| | | protected array $columns; |
| | | protected array $keys = []; |
| | | protected array $constraints =[]; |
| | | |
| | | /** @var array<string, self> Instance cache for fluent interface */ |
| | | protected static array $instances = []; |
| | | protected static string $charsetCollate; |
| | | |
| | | protected static string $userTable; |
| | | protected static string $userIDType; |
| | | protected static string $termIDType; |
| | | protected static string $postIDType; |
| | | |
| | | /** |
| | | * Fluent factory method |
| | |
| | | self::$instances[$tableName] = new self($tableName); |
| | | } |
| | | |
| | | |
| | | return self::$instances[$tableName]; |
| | | } |
| | | |
| | | public function ensureDefined() { |
| | | $this->ensureUserTable(); |
| | | $this->ensureUserIDType(); |
| | | $this->ensureTermIDType(); |
| | | $this->ensurePostIDType(); |
| | | } |
| | | protected function ensureUserTable():void |
| | | { |
| | | if (!isset(static::$userTable)) { |
| | | static::$userTable = is_multisite() ? $this->getMultisiteUsersTable() : $this->wpdb->users; |
| | | } |
| | | } |
| | | protected function getMultisiteUsersTable():string |
| | | { |
| | | $siteUsersTable = $this->wpdb->prefix . 'users'; |
| | | $siteExists = $this->wpdb->get_var( |
| | | $this->wpdb->prepare("SHOW TABLES LIKE %s", $siteUsersTable) |
| | | ); |
| | | if ($siteExists) { |
| | | return $siteUsersTable; |
| | | } |
| | | //fallback to main one |
| | | return $this->wpdb->users; |
| | | } |
| | | protected function ensureUserIDType():void |
| | | { |
| | | if (!isset(static::$userIDType)) { |
| | | $this->ensureUserTable(); |
| | | static::$userIDType = $this->getColumnType(static::$userTable, 'ID'); |
| | | } |
| | | } |
| | | |
| | | protected function ensureTermIDType():void |
| | | { |
| | | if (!isset(static::$termIDType)) { |
| | | static::$termIDType = $this->getColumnType($this->wpdb->terms, 'term_id'); |
| | | } |
| | | } |
| | | |
| | | protected function ensurePostIDType():void |
| | | { |
| | | if (!isset(static::$postIDType)) { |
| | | static::$postIDType = $this->getColumnType($this->wpdb->posts, 'ID'); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * |
| | | * @param array $columns An array of $columnName => $columnDefinition |
| | | * @return $this |
| | | */ |
| | | public function setColumns(array $columns):self |
| | | { |
| | | $this->columns = $columns; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * @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 |
| | | */ |
| | | public function setKeys(array $keys):self |
| | | { |
| | | $this->keys = $keys; |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * @param array $constraints an array of arrays, each value a $constraint => $references |
| | | * @return $this |
| | | */ |
| | | public function setConstraints(array $constraints):self |
| | | { |
| | | $this->constraints = $constraints; |
| | | return $this; |
| | | } |
| | | |
| | | public function defineTable(): self |
| | | { |
| | | if (empty($this->columns)) { |
| | | error_log('[CustomTable] No columns defined for ' . $this->tableName); |
| | | return $this; |
| | | } |
| | | |
| | | $parts = []; |
| | | |
| | | // Columns |
| | | foreach ($this->columns as $name => $type) { |
| | | $parts[] = "`{$name}` {$type}"; |
| | | } |
| | | |
| | | // Keys |
| | | if (empty($this->keys)) { |
| | | $parts[] = 'PRIMARY KEY (`' . array_key_first($this->columns) . '`)'; |
| | | } else { |
| | | foreach ($this->keys as $key) { |
| | | if (is_array($key)) { |
| | | $value = $key['value']; |
| | | // Ensure value is wrapped in parentheses |
| | | if (!str_starts_with(trim($value), '(')) { |
| | | $value = '(`' . $value . '`)'; |
| | | } |
| | | $parts[] = $key['key'] . ' KEY ' . $value; |
| | | } else { |
| | | $parts[] = 'KEY ' . $key; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Constraints |
| | | foreach ($this->constraints as $constraint => $references) { |
| | | $parts[] = "CONSTRAINT {$constraint} REFERENCES {$references}"; |
| | | } |
| | | |
| | | $this->definition = "(\n " . implode(",\n ", $parts) . "\n)"; |
| | | return $this; |
| | | } |
| | | |
| | | public static function ensureTables():void |
| | | { |
| | | require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); |
| | | |
| | | foreach (self::$instances as $instance) { |
| | | if (!$instance->wpdb->get_var("SHOW TABLES LIKE '{$instance->fullTableName}'")) { |
| | | $instance->createTable(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | protected function createTable():void |
| | | { |
| | | if (!$this->definition && empty($this->definition)) { |
| | | error_log('[CustomTable]No definition set for '.$this->tableName); |
| | | return; |
| | | } |
| | | $charset = self::$charsetCollate; |
| | | $schema = "CREATE TABLE IF NOT EXISTS {$this->fullTableName} {$this->definition} {$charset}"; |
| | | $this->wpdb->flush(); |
| | | |
| | | $hasForeignKey = stripos($schema, 'FOREIGN KEY') !== false; |
| | | if ($hasForeignKey) { |
| | | $result = $this->wpdb->query($schema); |
| | | $success = ($result !== false && !$this->wpdb->last_error); |
| | | } else { |
| | | dbDelta($schema.';'); |
| | | $success = !$this->wpdb->last_error; |
| | | } |
| | | |
| | | if (!$success) { |
| | | $error_msg = "SQL Error creating table {$this->tableName}: " . $this->wpdb->last_error; |
| | | error_log($error_msg); |
| | | error_log("Failed SQL Query: " . $schema); |
| | | } elseif ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) { |
| | | error_log("Successfully created table: {$this->fullTableName}"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Clear instance cache (useful for testing) |
| | | */ |
| | |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->tableName = $tableName; |
| | | $this->fullTableName = $wpdb->prefix . BASE . $tableName; |
| | | $this->fullTableName = $wpdb->prefix . apply_filters('jvb_base', BASE) . $tableName; |
| | | $this->useTransactions = $useTransactions; |
| | | |
| | | $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->wpdb->rows_affected; |
| | | } |
| | | |
| | | public function getUserIDType():string |
| | | { |
| | | $this->ensureUserIdType(); |
| | | return static::$userIDType; |
| | | } |
| | | public function getUserTable():string |
| | | { |
| | | $this->ensureUserTable(); |
| | | return static::$userTable; |
| | | } |
| | | public function getTermIDType():string |
| | | { |
| | | $this->ensureTermIDType(); |
| | | return static::$termIDType; |
| | | } |
| | | public function getPostIDType():string |
| | | { |
| | | $this->ensurePostIDType(); |
| | | return static::$postIDType; |
| | | } |
| | | |
| | | // ========================================================================= |
| | | // PRIVATE HELPERS |
| | | // ========================================================================= |
| | | private function getColumnType(string $table, string $column):string|false { |
| | | $tableExists = $this->wpdb->get_var( |
| | | $this->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( |
| | | "SELECT COLUMN_TYPE |
| | | FROM INFORMATION_SCHEMA.COLUMNS |
| | | WHERE TABLE_SCHEMA = DATABASE() |
| | | AND TABLE_NAME = %s |
| | | AND COLUMN_NAME = %s", |
| | | $table, |
| | | $column |
| | | ) |
| | | ); |
| | | if ($result && isset($result->COLUMN_TYPE)) { |
| | | return $result->COLUMN_TYPE; |
| | | } |
| | | error_log("[CustomTable] Could not determine column type for {$table}.{$column}"); |
| | | return 'bigint(20) unsigned'; |
| | | } |
| | | |
| | | private function tableExists():bool |
| | | { |
| | | return !is_null($this->wpdb->get_var($this->wpdb->prepare("SHOW TABLES LIKE %s", $this->fullTableName))); |
| | | } |
| | | /** |
| | | * Build WHERE clause from associative array |
| | | */ |