Clean-room copy of the reusable engines from comiida, with all instance data, secrets, dependencies, and build output excluded: - app/ Astro theme skeleton (no comiida blog posts; hero image -> placeholder) - api/ SeedProject PHP framework (no vendor/.env/config.php) - content-pipeline/ engine only (scripts/admin/prompts; empty runtime state) - astroagent.config.json + app/.astroagent/skills Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
366 lines
10 KiB
PHP
366 lines
10 KiB
PHP
<?php
|
|
|
|
class Db
|
|
{
|
|
private static $_connections = [];
|
|
private static $_activeConnection = null;
|
|
|
|
protected static $_fetchMode = \PDO::FETCH_ASSOC;
|
|
protected static $_driverOptions = [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8; SET time_zone = 'America/New_York'"
|
|
];
|
|
|
|
/**
|
|
* Set connection information
|
|
*
|
|
* @example Db::setConnectionInfo('mysql', 'dbname', 'username', 'password', 'hostname', 'connectionName');
|
|
*/
|
|
public static function setConnectionInfo(
|
|
$driver,
|
|
$dbname,
|
|
$username,
|
|
$password,
|
|
$hostname = 'localhost',
|
|
$connectionName = 'default'
|
|
) {
|
|
self::$_connections[$connectionName] = [
|
|
'driver' => $driver,
|
|
'connectionString' => "{$driver}:dbname={$dbname};host={$hostname}",
|
|
'username' => $username,
|
|
'password' => $password,
|
|
];
|
|
|
|
if (self::$_activeConnection === null) {
|
|
self::$_activeConnection = $connectionName;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Switch to a different connection
|
|
*/
|
|
public static function useConnection($connectionName)
|
|
{
|
|
if (!isset(self::$_connections[$connectionName])) {
|
|
throw new \Exception("Connection '{$connectionName}' does not exist.");
|
|
}
|
|
self::$_activeConnection = $connectionName;
|
|
}
|
|
|
|
/**
|
|
* Get the current PDO object
|
|
*/
|
|
public static function getPDOObject()
|
|
{
|
|
return self::_getConnection();
|
|
}
|
|
|
|
/**
|
|
* Execute a statement and returns the first row
|
|
*/
|
|
public static function getRow($sql, $params = [])
|
|
{
|
|
$statement = self::_query($sql, $params);
|
|
return $statement->fetch(self::$_fetchMode);
|
|
}
|
|
|
|
/**
|
|
* Execute a statement and returns all rows
|
|
*/
|
|
public static function select($sql, $params = [])
|
|
{
|
|
$statement = self::_query($sql, $params);
|
|
return $statement->fetchAll(self::$_fetchMode);
|
|
}
|
|
|
|
|
|
/**
|
|
* Insert a new row into the database
|
|
*/
|
|
public static function insert($table, $data)
|
|
{
|
|
$pdo = self::_getConnection();
|
|
|
|
ksort($data);
|
|
$fieldNames = implode('`, `', array_keys($data));
|
|
$fieldValues = ':' . implode(', :', array_keys($data));
|
|
|
|
try {
|
|
$sth = $pdo->prepare("INSERT INTO $table (`$fieldNames`) VALUES ($fieldValues)");
|
|
|
|
foreach ($data as $key => $value) {
|
|
$sth->bindValue(":$key", $value);
|
|
}
|
|
|
|
$sth->execute();
|
|
|
|
return json_encode([
|
|
'Code' => 1,
|
|
'Message' => 'Created',
|
|
'ID' => $pdo->lastInsertId()
|
|
]);
|
|
} catch (\PDOException $e) {
|
|
return json_encode([
|
|
'Code' => 0,
|
|
'Message' => 'Error Creating Entry: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update existing rows in the database
|
|
*/
|
|
public static function update($table, $data, $where, $whereParams = [])
|
|
{
|
|
$pdo = self::_getConnection();
|
|
|
|
ksort($data);
|
|
$usesPositionalWhere = str_contains($where, '?');
|
|
|
|
if ($usesPositionalWhere) {
|
|
$fieldDetails = implode(', ', array_map(function($key) {
|
|
return "`$key` = ?";
|
|
}, array_keys($data)));
|
|
} else {
|
|
$fieldDetails = implode(', ', array_map(function($key) {
|
|
return "`$key` = :set_$key";
|
|
}, array_keys($data)));
|
|
}
|
|
|
|
try {
|
|
$sth = $pdo->prepare("UPDATE $table SET $fieldDetails WHERE $where");
|
|
|
|
if ($usesPositionalWhere) {
|
|
// Single positional style: SET values first, WHERE values after.
|
|
$params = array_values($data);
|
|
if (!empty($whereParams)) {
|
|
$params = array_merge($params, array_values($whereParams));
|
|
}
|
|
$sth->execute($params);
|
|
} else {
|
|
// Single named style for SET; allow named WHERE params if supplied.
|
|
$params = [];
|
|
foreach ($data as $key => $value) {
|
|
$params["set_$key"] = $value;
|
|
}
|
|
foreach ($whereParams as $key => $value) {
|
|
$params[ltrim((string)$key, ':')] = $value;
|
|
}
|
|
$sth->execute($params);
|
|
}
|
|
|
|
$count = $sth->rowCount();
|
|
|
|
return json_encode([
|
|
'Code' => ($count > 0) ? 1 : 0,
|
|
'Rows' => $count,
|
|
'Message' => ($count > 0) ? 'Updated' : 'No Records Updated'
|
|
]);
|
|
} catch (\PDOException $e) {
|
|
return json_encode([
|
|
'Code' => $e->errorInfo[1],
|
|
'Message' => 'Error Updating Database: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute a statement and returns number of affected rows
|
|
*/
|
|
public static function execute($sql, $params = [])
|
|
{
|
|
$statement = self::_query($sql, $params);
|
|
return $statement->rowCount();
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Execute a statement and returns a single value
|
|
*/
|
|
public static function getValue($sql, $params = [])
|
|
{
|
|
$statement = self::_query($sql, $params);
|
|
return $statement->fetchColumn(0);
|
|
}
|
|
|
|
/**
|
|
* Set PDO fetch mode
|
|
*/
|
|
public static function setFetchMode($fetchMode)
|
|
{
|
|
self::$_fetchMode = $fetchMode;
|
|
}
|
|
|
|
/**
|
|
* Begin a transaction
|
|
*/
|
|
public static function beginTransaction()
|
|
{
|
|
self::_getConnection()->beginTransaction();
|
|
}
|
|
|
|
/**
|
|
* Commit a transaction
|
|
*/
|
|
public static function commitTransaction()
|
|
{
|
|
self::_getConnection()->commit();
|
|
}
|
|
|
|
/**
|
|
* Rollback a transaction
|
|
*/
|
|
public static function rollbackTransaction()
|
|
{
|
|
self::_getConnection()->rollBack();
|
|
}
|
|
|
|
/**
|
|
* Backward-compatible aliases used across controllers.
|
|
*/
|
|
public static function commit()
|
|
{
|
|
self::commitTransaction();
|
|
}
|
|
|
|
public static function rollback()
|
|
{
|
|
self::rollbackTransaction();
|
|
}
|
|
|
|
/**
|
|
* Set PDO driver options
|
|
*/
|
|
public static function setDriverOptions(array $options)
|
|
{
|
|
self::$_driverOptions = $options;
|
|
}
|
|
|
|
/**
|
|
* Get or create the PDO connection
|
|
*/
|
|
private static function _getConnection()
|
|
{
|
|
$connectionInfo = self::$_connections[self::$_activeConnection];
|
|
|
|
if (!isset($connectionInfo['pdo'])) {
|
|
$connectionInfo['pdo'] = new \PDO(
|
|
$connectionInfo['connectionString'],
|
|
$connectionInfo['username'],
|
|
$connectionInfo['password'],
|
|
self::$_driverOptions
|
|
);
|
|
self::$_connections[self::$_activeConnection] = $connectionInfo;
|
|
}
|
|
|
|
return $connectionInfo['pdo'];
|
|
}
|
|
|
|
/**
|
|
* Prepare and execute a PDO statement
|
|
*/
|
|
private static function _query($sql, $params = [])
|
|
{
|
|
$pdo = self::_getConnection();
|
|
$statement = $pdo->prepare($sql);
|
|
|
|
if (!$statement) {
|
|
$errorInfo = $pdo->errorInfo();
|
|
throw new \PDOException("Database error [{$errorInfo[0]}]: {$errorInfo[2]}, driver error code is $errorInfo[1]");
|
|
}
|
|
|
|
if (!$statement->execute($params) || $statement->errorCode() != '00000') {
|
|
$errorInfo = $statement->errorInfo();
|
|
throw new \PDOException("Database error [{$errorInfo[0]}]: {$errorInfo[2]}, driver error code is $errorInfo[1]");
|
|
}
|
|
|
|
return $statement;
|
|
}
|
|
|
|
/**
|
|
* Delete rows from the database
|
|
*/
|
|
public static function delete($table, $where)
|
|
{
|
|
$pdo = self::_getConnection();
|
|
|
|
// Build the WHERE clause dynamically
|
|
$whereConditions = implode(' AND ', array_map(function($key) {
|
|
return "`$key` = :$key";
|
|
}, array_keys($where)));
|
|
|
|
try {
|
|
$sth = $pdo->prepare("DELETE FROM $table WHERE $whereConditions");
|
|
|
|
// Bind the parameters from the where array
|
|
foreach ($where as $key => $value) {
|
|
$sth->bindValue(":$key", $value);
|
|
}
|
|
|
|
$sth->execute();
|
|
$count = $sth->rowCount();
|
|
|
|
return json_encode([
|
|
'Code' => ($count > 0) ? 1 : 0,
|
|
'Rows' => $count,
|
|
'Message' => ($count > 0) ? 'Deleted' : 'No Records Deleted'
|
|
]);
|
|
} catch (\PDOException $e) {
|
|
return json_encode([
|
|
'Code' => $e->errorInfo[1],
|
|
'Message' => 'Error Deleting From Database: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
public static function batchInsert($table, array $data)
|
|
{
|
|
if (empty($data)) {
|
|
return json_encode([
|
|
'Code' => 0,
|
|
'Message' => 'No data provided for insertion'
|
|
]);
|
|
}
|
|
|
|
$pdo = self::_getConnection();
|
|
|
|
// Assume all rows have the same structure as the first row
|
|
$firstRow = reset($data);
|
|
$columns = array_keys($firstRow);
|
|
$columnString = '`' . implode('`, `', $columns) . '`';
|
|
|
|
// Create placeholders for each row
|
|
$rowPlaceholder = '(' . implode(', ', array_fill(0, count($columns), '?')) . ')';
|
|
$valuePlaceholders = implode(', ', array_fill(0, count($data), $rowPlaceholder));
|
|
|
|
$sql = "INSERT INTO $table ($columnString) VALUES $valuePlaceholders";
|
|
|
|
try {
|
|
$stmt = $pdo->prepare($sql);
|
|
|
|
// Flatten the data array and bind values
|
|
$values = [];
|
|
foreach ($data as $row) {
|
|
foreach ($columns as $column) {
|
|
$values[] = $row[$column];
|
|
}
|
|
}
|
|
|
|
$stmt->execute($values);
|
|
|
|
$insertedCount = $stmt->rowCount();
|
|
|
|
return json_encode([
|
|
'Code' => 1,
|
|
'Message' => 'Batch insert successful',
|
|
'InsertedRows' => $insertedCount
|
|
]);
|
|
} catch (\PDOException $e) {
|
|
return json_encode([
|
|
'Code' => 0,
|
|
'Message' => 'Error performing batch insert: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
}
|