Jake Vanderwerf
2026-03-03 772462eeca3002a1d52508aeba485aab2b4742ad
inc/managers/CustomTable.php
@@ -21,11 +21,21 @@
{
   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
@@ -41,9 +51,167 @@
         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)
    */
@@ -61,8 +229,13 @@
      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}";
   }
   // =========================================================================
@@ -678,10 +851,61 @@
      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
    */