From 0afb2c0046b55c123eafb4ab9ee77efa68d12463 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 06 Jun 2026 17:15:31 +0000
Subject: [PATCH] =Starting the Favourites.js setup, converting previous Northeh stuff to new Registrar, fixing up Square.php integration to match
---
inc/managers/CustomTable.php | 461 ++++++++++++++++++++++++++++++++++++++++++++++++++-------
1 files changed, 405 insertions(+), 56 deletions(-)
diff --git a/inc/managers/CustomTable.php b/inc/managers/CustomTable.php
index 893e5ad..572d8a2 100644
--- a/inc/managers/CustomTable.php
+++ b/inc/managers/CustomTable.php
@@ -21,11 +21,25 @@
{
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 $termTable;
+ protected static string $termIDType;
+ protected static string $postIDType;
+ protected static string $postTable;
+
+ protected Cache $cache;
/**
* Fluent factory method
@@ -35,15 +49,206 @@
*
* @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);
}
+
return self::$instances[$tableName];
}
+ public static function destroyInstance(string $tableName):void
+ {
+ if (isset(self::$instances[$tableName])) {
+ unset(self::$instances[$tableName]);
+ }
+ }
+ public static function getInstance(string $tableName):self|false
+ {
+ if (!isset(self::$instances[$tableName])) {
+ return false;
+ }
+ 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 ensureTermTable():void
+ {
+ if (!isset(static::$termTable)) {
+ static::$termTable = $this->wpdb->terms;
+ }
+ }
+
+ protected function ensurePostIDType():void
+ {
+ if (!isset(static::$postIDType)) {
+ static::$postIDType = $this->getColumnType($this->wpdb->posts, 'ID');
+ }
+ }
+ protected function ensurePostTable():void
+ {
+ if (!isset(static::$postTable)) {
+ static::$postTable = $this->wpdb->posts;
+ }
+ }
+
+ /**
+ *
+ * @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_contains(trim($value), '(`')) {
+ $value = '(`' . $value . '`)';
+ }
+ $parts[] = $key['key'] . ' KEY ' . $value;
+ } else {
+ $parts[] = 'KEY ' . $key;
+ }
+ }
+ }
+
+ // Constraints
+ foreach ($this->constraints as $constraint) {
+ $parts[] = $constraint;
+ }
+
+ $this->definition = "(\n " . implode(",\n ", $parts) . "\n)";
+ $this->createTable();
+ 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;
+ }
+ if ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) {
+ 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();
+
+ $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)
*/
@@ -56,13 +261,26 @@
* @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)
+ public function __construct(string $tableName, bool $user = false, bool $auth = false, bool $useTransactions = false)
{
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;
+
+ $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}";
+
}
// =========================================================================
@@ -236,6 +454,8 @@
return false;
}
+ $this->cache->flush();
+
return $this->wpdb->insert_id;
}
@@ -266,26 +486,19 @@
* );
* // Returns: ['id' => 456, 'created' => false, 'record' => object]
*/
- public function findOrCreate(array $searchData, array $createData = []): array
+ public function findOrCreate(array $searchData, array $createData = []):int|false
{
$record = $this->get($searchData);
if ($record) {
- return [
- 'id' => $record->id ?? 0,
- 'created' => false,
- 'record' => $record
- ];
+ if (!empty($createData)) {
+ return $this->update($createData, $searchData);
+ }
+ return $record->id;
}
$data = array_merge($searchData, $createData);
- $id = $this->insert($data);
-
- return [
- 'id' => $id,
- 'created' => true,
- 'record' => $this->get(['id' => $id])
- ];
+ return $this->insert($data);
}
/**
@@ -363,10 +576,17 @@
*/
public function get(array $where, string $output = OBJECT): object|array|null
{
- $query = "SELECT * FROM {$this->fullTableName} WHERE " . $this->buildWhereClause($where);
- $values = array_values($where);
+ return $this->cache->remember(
+ $this->cache->generateKey(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 $this->wpdb->get_row($this->wpdb->prepare($query, $values), $output);
+ }
+ );
}
/**
@@ -386,34 +606,76 @@
*/
public function getMany(array $args = [], string $output = OBJECT): array
{
- $query = "SELECT * FROM {$this->fullTableName}";
- $values = [];
+ return $this->cache->remember(
+ $this->cache->generateKey(array_merge($args, ['output' => $output])),
+ function () use ($args, $output) {
+ $query = "SELECT * FROM {$this->fullTableName}";
+ $values = [];
- // WHERE clause
- if (!empty($args['where'])) {
- $query .= " WHERE " . $this->buildWhereClause($args['where']);
- $values = array_merge($values, array_values($args['where']));
- }
+ // WHERE clause
+ if (!empty($args['where'])) {
+ $clause = $this->buildWhereClause($args['where']);
- // ORDER BY
- if (!empty($args['orderby'])) {
- $orderby = sanitize_sql_orderby($args['orderby']);
- $order = (!empty($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC';
- $query .= " ORDER BY {$orderby} {$order}";
- }
+ $query .= " WHERE {$clause['sql']}";
+ $values = array_merge($values, $clause['values']);
+ }
- // LIMIT
- if (!empty($args['limit'])) {
- $limit = absint($args['limit']);
- $offset = !empty($args['offset']) ? absint($args['offset']) : 0;
- $query .= " LIMIT {$offset}, {$limit}";
- }
+ // ORDER BY
+ if (!empty($args['orderby'])) {
+ $orderby = sanitize_sql_orderby($args['orderby']);
+ $order = (!empty($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC';
+ $query .= " ORDER BY {$orderby} {$order}";
+ }
- if (empty($values)) {
- return $this->wpdb->get_results($query, $output);
- }
+ // LIMIT
+ if (!empty($args['limit'])) {
+ $limit = absint($args['limit']);
+ $offset = !empty($args['offset']) ? absint($args['offset']) : 0;
+ $query .= " LIMIT {$offset}, {$limit}";
+ }
- return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output);
+ if (empty($values)) {
+ return $this->wpdb->get_results($query, $output);
+ }
+
+ return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output);
+ }
+ );
+
+ }
+
+ /**
+ * Get a specific column value from all matches
+ * @param string $column
+ * @param array $where
+ * @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),
+ 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"]);
+ return [];
+ }
+ $args = ['where' => $where];
+ if ($orderby) {
+ $args['orderby'] = $orderby;
+ }
+ if ($order) {
+ $args['order'] = $order;
+ }
+ if ($limit) {
+ $args['limit'] = $limit;
+ }
+
+ return array_column($this->getMany($args), $column);
+ }
+ );
+
}
/**
@@ -424,19 +686,26 @@
*/
public function count(array $where = []): int
{
- $query = "SELECT COUNT(*) FROM {$this->fullTableName}";
- $values = [];
+ return $this->cache->remember(
+ $this->cache->generateKey(array_merge(['source' => 'count'], $where)),
+ function() use ($where) {
+ $query = "SELECT COUNT(*) FROM {$this->fullTableName}";
+ $values = [];
- if (!empty($where)) {
- $query .= " WHERE " . $this->buildWhereClause($where);
- $values = array_values($where);
- }
+ if (!empty($where)) {
+ $clause = $this->buildWhereClause($where);
+ $query .= " WHERE {$clause['sql']}";
+ $values = $clause['values'];
+ }
- if (empty($values)) {
- return (int) $this->wpdb->get_var($query);
- }
+ if (empty($values)) {
+ return (int) $this->wpdb->get_var($query);
+ }
- return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $values));
+ return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $values));
+ }
+ );
+
}
/**
@@ -487,6 +756,7 @@
if ($result === false) {
$this->logError('update', ['data' => $data, 'where' => $where]);
}
+ $this->cache->flush();
return $result;
}
@@ -516,6 +786,7 @@
if ($result === false) {
$this->logError('delete', ['where' => $where]);
}
+ $this->cache->flush();
return $result;
}
@@ -638,7 +909,7 @@
} catch (Exception $e) {
$this->rollback();
$this->logError('transaction', ['error' => $e->getMessage()]);
- throw $e;
+ return false;
}
}
@@ -678,25 +949,103 @@
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 getTermTable():string
+ {
+ $this->ensureTermTable();
+ return static::$termTable;
+ }
+ public function getPostIDType():string
+ {
+ $this->ensurePostIDType();
+ return static::$postIDType;
+ }
+ public function getPostTable():string
+ {
+ $this->ensurePostTable();
+ return static::$postTable;
+ }
+
// =========================================================================
// 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
*/
- private function buildWhereClause(array $where): string
+ private function buildWhereClause(array $where):array
{
$conditions = [];
+ $values = [];
foreach ($where as $column => $value) {
$column_safe = esc_sql($column);
+
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)) {
+ $placeholders = implode(',', array_map([$this, 'getPlaceholder'], $operand));
+ $conditions[] = "`{$column_safe}` {$operator} ({$placeholders})";
+ $values = array_merge($values, $operand);
+ } else {
+ // <, >, <=, >=, !=, LIKE, etc.
+ $conditions[] = "`{$column_safe}` {$operator} " . $this->getPlaceholder($operand);
+ $values[] = $operand;
+ }
} else {
$conditions[] = "`{$column_safe}` = " . $this->getPlaceholder($value);
+ $values[] = $value;
}
}
- return implode(' AND ', $conditions);
+
+ return ['sql' => implode(' AND ', $conditions), 'values' => $values];
}
/**
--
Gitblit v1.10.0