mirror of
https://github.com/NamelessMC/Nameless
synced 2026-08-18 06:29:04 -04:00
* Convert reports to Notification system * fix unused term check for LanguageKey class * simplify language#get * remove n+1 * fix for multiple * Convert Forum topic replies with Notifications * review amendments * fixes * Apply fixes from StyleCI (#3627) Co-authored-by: StyleCI Bot <bot@styleci.io> * add back existing_alerts * remove unneeded exception * add migrations * fix * lint * Convert Mentions to Notification system * fixes * wip * wip - code comments + fix mention parsing for custom page editing * rename variable * cleanup MentionsHook * cleanup * remove unused term * Apply fixes from StyleCI (#3634) Co-authored-by: StyleCI Bot <bot@styleci.io> * don't notify blocked users, but still tag them * undo changes to DB class * trailing comma --------- Co-authored-by: StyleCI Bot <bot@styleci.io>
588 lines
17 KiB
PHP
588 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Creates a singleton connection to the database with credentials from the config file.
|
|
*
|
|
* @package NamelessMC\Database
|
|
* @author Samerton
|
|
* @version 2.0.0-pr13
|
|
* @license MIT
|
|
*/
|
|
class DB
|
|
{
|
|
private static ?DB $_instance = null;
|
|
|
|
private string $_prefix;
|
|
private ?string $_force_charset;
|
|
private ?string $_force_collation;
|
|
protected PDO $_pdo;
|
|
private PDOStatement $_statement;
|
|
private bool $_error = false;
|
|
private array $_results;
|
|
private int $_count = 0;
|
|
protected QueryRecorder $_query_recorder;
|
|
|
|
private function __construct(
|
|
string $host,
|
|
string $database,
|
|
string $username,
|
|
string $password,
|
|
int $port,
|
|
?string $force_charset,
|
|
?string $force_collation,
|
|
string $prefix
|
|
) {
|
|
$this->_force_charset = $force_charset;
|
|
$this->_force_collation = $force_collation;
|
|
$this->_prefix = $prefix;
|
|
|
|
$connection_string = 'mysql:host=' . $host . ';port=' . $port . ';dbname=' . $database;
|
|
if ($force_charset) {
|
|
$connection_string .= ';charset=' . $force_charset;
|
|
}
|
|
$this->_pdo = new PDO(
|
|
$connection_string,
|
|
$username,
|
|
$password,
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
]
|
|
);
|
|
|
|
$this->_query_recorder = QueryRecorder::getInstance();
|
|
}
|
|
|
|
public static function getCustomInstance(
|
|
string $host,
|
|
string $database,
|
|
string $username,
|
|
string $password,
|
|
int $port = 3306,
|
|
?string $force_charset = null,
|
|
?string $force_collation = null,
|
|
string $prefix = 'nl2_'
|
|
): DB {
|
|
return new DB(
|
|
$host,
|
|
$database,
|
|
$username,
|
|
$password,
|
|
$port,
|
|
$force_charset,
|
|
$force_collation,
|
|
$prefix
|
|
);
|
|
}
|
|
|
|
public static function getInstance(): DB
|
|
{
|
|
if (self::$_instance) {
|
|
return self::$_instance;
|
|
}
|
|
|
|
if (Config::get('mysql.initialise_charset')) {
|
|
$force_charset = Config::get('mysql.charset') ?: 'utf8mb4';
|
|
} else {
|
|
$force_charset = null;
|
|
}
|
|
|
|
if (Config::get('mysql.initialise_collation')) {
|
|
$force_collation = Config::get('mysql.collation') ?: 'utf8mb4_unicode_ci';
|
|
} else {
|
|
$force_collation = null;
|
|
}
|
|
|
|
return self::$_instance = self::getCustomInstance(
|
|
Config::get('mysql.host'),
|
|
Config::get('mysql.db'),
|
|
Config::get('mysql.username'),
|
|
Config::get('mysql.password'),
|
|
Config::get('mysql.port'),
|
|
$force_charset,
|
|
$force_collation
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the underlying PDO instance.
|
|
*
|
|
* @return PDO The PDO instance.
|
|
*/
|
|
public function getPDO(): PDO
|
|
{
|
|
return $this->_pdo;
|
|
}
|
|
|
|
/**
|
|
* Begin a MySQL transaction.
|
|
*/
|
|
public function beginTransaction(): void
|
|
{
|
|
$this->_pdo->beginTransaction();
|
|
}
|
|
|
|
/**
|
|
* Commit a MySQL transaction.
|
|
*/
|
|
public function commitTransaction(): void
|
|
{
|
|
if ($this->_pdo->inTransaction()) {
|
|
$this->_pdo->commit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Roll back a MySQL transaction.
|
|
*/
|
|
public function rollBackTransaction(): void
|
|
{
|
|
if ($this->_pdo->inTransaction()) {
|
|
$this->_pdo->rollBack();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute a database query within a MySQL transaction, and get the results of the query, if any.
|
|
*
|
|
* @param Closure(DB): mixed $closure The closure to pass this instance to and execute within a transaction context.
|
|
* @return mixed The results of the query, null if none.
|
|
*/
|
|
public function transaction(Closure $closure)
|
|
{
|
|
$result = null;
|
|
|
|
try {
|
|
$this->beginTransaction();
|
|
|
|
$result = $closure($this);
|
|
|
|
$this->commitTransaction();
|
|
} catch (Exception $exception) {
|
|
$this->rollBackTransaction();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get the first row of the result.
|
|
*
|
|
* @return object|null The result object, or null if no result was returned.
|
|
*/
|
|
public function first(): ?object
|
|
{
|
|
return $this->results()[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get all the results of the query.
|
|
*
|
|
* @return array The results of the query.
|
|
*/
|
|
public function results(): array
|
|
{
|
|
return $this->_results;
|
|
}
|
|
|
|
/**
|
|
* Get the number of rows affected by the last query.
|
|
*
|
|
* @return int The number of rows.
|
|
*/
|
|
public function count(): int
|
|
{
|
|
return $this->_count;
|
|
}
|
|
|
|
/**
|
|
* Get whether any results exist.
|
|
*
|
|
* @return bool Whether any results exist.
|
|
*/
|
|
public function exists(): bool
|
|
{
|
|
return $this->_count > 0;
|
|
}
|
|
|
|
/**
|
|
* Get the last inserted ID.
|
|
*
|
|
* @return string|false ID of the last inserted row or false on failure
|
|
*/
|
|
public function lastId()
|
|
{
|
|
return $this->_pdo->lastInsertId();
|
|
}
|
|
|
|
/**
|
|
* Whether there was an error during the last query.
|
|
*
|
|
* @return bool Whether there was an error.
|
|
*/
|
|
public function error(): bool
|
|
{
|
|
return $this->_error;
|
|
}
|
|
|
|
/**
|
|
* Perform a SELECT query on the database.
|
|
*
|
|
* @param string $table The table to select from.
|
|
* @param mixed $where The where clause. If not an array, it will be used for "id" column lookup.
|
|
* @return static|false This instance if successful, false otherwise.
|
|
*/
|
|
public function get(string $table, $where = [])
|
|
{
|
|
if (!is_array($where)) {
|
|
$where = ['id', '=', $where];
|
|
}
|
|
|
|
return $this->action('SELECT *', $table, $where);
|
|
}
|
|
|
|
/**
|
|
* Perform a DELETE query on the database.
|
|
*
|
|
* @param string $table The table to delete from.
|
|
* @param mixed $where The where clause. If not an array, it will be used for "id" column lookup.
|
|
* @return static|false This instance if successful, false otherwise.
|
|
*/
|
|
public function delete(string $table, $where)
|
|
{
|
|
if (!is_array($where)) {
|
|
$where = ['id', '=', $where];
|
|
}
|
|
|
|
return $this->action('DELETE', $table, $where);
|
|
}
|
|
|
|
/**
|
|
* Perform a raw SQL query on the database.
|
|
*
|
|
* @param string $sql The SQL query string to execute.
|
|
* @param array $params The parameters to bind to the query.
|
|
* @param bool $isSelect Whether the statement is a select, defaults to null
|
|
* @return static This DB instance.
|
|
*/
|
|
public function query(string $sql, array $params = [], ?bool $isSelect = null)
|
|
{
|
|
$this->_error = false;
|
|
if ($this->_statement = $this->_pdo->prepare($sql)) {
|
|
$x = 1;
|
|
foreach ($params as $param) {
|
|
// Convert "true" and "false" to 1 and 0 so that query params can be more fluent
|
|
if (is_bool($param)) {
|
|
$param = $param ? 1 : 0;
|
|
}
|
|
$this->_statement->bindValue(
|
|
$x,
|
|
$param,
|
|
is_int($param)
|
|
? PDO::PARAM_INT
|
|
: PDO::PARAM_STR
|
|
);
|
|
$x++;
|
|
}
|
|
|
|
$this->_query_recorder->pushQuery($sql, $params);
|
|
|
|
if ($this->_statement->execute()) {
|
|
// Only fetch the results if this is a SELECT query.
|
|
if ($isSelect || str_starts_with(strtoupper(ltrim($sql)), 'SELECT')) {
|
|
$this->_results = $this->_statement->fetchAll(PDO::FETCH_OBJ);
|
|
}
|
|
$this->_count = $this->_statement->rowCount();
|
|
} else {
|
|
print_r($this->_pdo->errorInfo());
|
|
$this->_error = true;
|
|
}
|
|
} else {
|
|
$this->_results = [];
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Execute some SQL action (which uses a where clause) on the database.
|
|
*
|
|
* @param string $action The action to perform (SELECT, DELETE).
|
|
* @param string $table The table to perform the action on.
|
|
* @param array $where The where clause.
|
|
* @return static|false This instance if successful, false otherwise.
|
|
*/
|
|
private function action(string $action, string $table, array $where = [])
|
|
{
|
|
[$where, $where_params] = self::makeWhere($where);
|
|
|
|
$table = $this->_prefix . $table;
|
|
$sql = "{$action} FROM {$table} {$where}";
|
|
|
|
if (!$this->query($sql, $where_params)->error()) {
|
|
return $this;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Insert a new row into a table within the database.
|
|
*
|
|
* @param string $table The table to insert into.
|
|
* @param array $fields Array of data in "column => value" format to insert.
|
|
* @return bool Whether an error occurred or not.
|
|
*/
|
|
public function insert(string $table, array $fields = []): bool
|
|
{
|
|
$keys = array_keys($fields);
|
|
$fieldCount = count($fields);
|
|
$values = '';
|
|
$x = 1;
|
|
|
|
for ($i = 0; $i < $fieldCount; $i++) {
|
|
$values .= '?';
|
|
if ($x < $fieldCount) {
|
|
$values .= ', ';
|
|
}
|
|
$x++;
|
|
}
|
|
|
|
$table = $this->_prefix . $table;
|
|
$sql = "INSERT INTO {$table} (`" . implode('`,`', $keys) . "`) VALUES ({$values})";
|
|
|
|
return !$this->query($sql, $fields)->error();
|
|
}
|
|
|
|
/**
|
|
* Perform an UPDATE query on a table.
|
|
*
|
|
* @param string $table The table to update.
|
|
* @param mixed $where The where clause. If not an array, it will be used for "id" column lookup.
|
|
* @param array $fields Array of data in "column => value" format to update.
|
|
* @return bool Whether an error occurred or not.
|
|
*/
|
|
public function update(string $table, $where, array $fields): bool
|
|
{
|
|
$set = '';
|
|
$x = 1;
|
|
|
|
foreach (array_keys($fields) as $column) {
|
|
$set .= "`{$column}` = ?";
|
|
if ($x < count($fields)) {
|
|
$set .= ', ';
|
|
}
|
|
$x++;
|
|
}
|
|
|
|
if (!is_array($where)) {
|
|
$where = ['id', '=', $where];
|
|
}
|
|
|
|
[$where, $where_params] = self::makeWhere($where);
|
|
$table = $this->_prefix . $table;
|
|
|
|
$sql = "UPDATE {$table} SET {$set} $where";
|
|
|
|
return !$this->query($sql, array_merge($fields, $where_params))->error();
|
|
}
|
|
|
|
/**
|
|
* Increment a numeric column value by 1.
|
|
*
|
|
* @param string $table The table to use.
|
|
* @param int $id The id of the row to increment a column in.
|
|
* @param string $field The field to increment.
|
|
* @return bool Whether an error occurred or not.
|
|
*/
|
|
public function increment(string $table, int $id, string $field): bool
|
|
{
|
|
$table = $this->_prefix . $table;
|
|
|
|
return !$this->query("UPDATE {$table} SET {$field} = {$field} + 1 WHERE id = ?", [$id])->error();
|
|
}
|
|
|
|
/**
|
|
* Decrement a numeric column value by 1.
|
|
*
|
|
* @param string $table The table to use.
|
|
* @param int $id The id of the row to decrement a column in.
|
|
* @param string $field The field to increment.
|
|
* @return bool Whether an error occurred or not.
|
|
*/
|
|
public function decrement(string $table, int $id, string $field): bool
|
|
{
|
|
$table = $this->_prefix . $table;
|
|
|
|
return !$this->query("UPDATE {$table} SET {$field} = {$field} - 1 WHERE id = ?", [$id])->error();
|
|
}
|
|
|
|
/**
|
|
* Select rows from the database, ordering by a specific column and sort type.
|
|
*
|
|
* @param string $table The table to use.
|
|
* @param string $order The column to order by.
|
|
* @param string $sort ASC or DESC
|
|
* @return static|false This instance if successful, false otherwise.
|
|
*/
|
|
public function orderAll(string $table, string $order, string $sort)
|
|
{
|
|
$table = $this->_prefix . $table;
|
|
$sql = "SELECT * FROM {$table} ORDER BY {$order} {$sort}";
|
|
|
|
if (!$this->query($sql)->error()) {
|
|
return $this;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Select rows from the database with a where clause, ordering by a specific column and sort type.
|
|
*
|
|
* @param string $table The table to use.
|
|
* @param string $order The column to order by.
|
|
* @param string $sort ASC or DESC
|
|
* @return static|false This instance if successful, false otherwise.
|
|
*/
|
|
public function orderWhere(string $table, string $where, string $order, string $sort)
|
|
{
|
|
$table = $this->_prefix . $table;
|
|
$sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY {$order} {$sort}";
|
|
|
|
if (!$this->query($sql)->error()) {
|
|
return $this;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create a new table in the database.
|
|
*
|
|
* @param string $name The name of the table.
|
|
* @param string $table_schema The table SQL schema.
|
|
* @return bool Whether an error occurred or not.
|
|
*/
|
|
public function createTable(string $name, string $table_schema): bool
|
|
{
|
|
$name = $this->_prefix . $name;
|
|
$sql = "CREATE TABLE `{$name}` ({$table_schema}) ENGINE=InnoDB";
|
|
|
|
if ($this->_force_charset) {
|
|
$sql .= ' DEFAULT CHARSET=' . $this->_force_charset;
|
|
}
|
|
|
|
if ($this->_force_collation) {
|
|
$sql .= ' COLLATE=' . $this->_force_collation;
|
|
}
|
|
|
|
return !$this->query($sql)->error();
|
|
}
|
|
|
|
/**
|
|
* Perform a SHOW TABLES LIKE query.
|
|
*
|
|
* @param string $table Name of table to try and lookup.
|
|
* @return int|false The number of rows affected, or false on failure.
|
|
*/
|
|
public function showTables(string $table)
|
|
{
|
|
$table = $this->_prefix . $table;
|
|
$sql = "SHOW TABLES LIKE '{$table}'";
|
|
|
|
if (!$this->query($sql)->error()) {
|
|
return $this->_statement->rowCount();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add a new column to a table.
|
|
*
|
|
* @param string $table Name of table to alter.
|
|
* @param string $column The column to add.
|
|
* @param string $attributes The attributes of the column.
|
|
* @return bool Whether an error occurred or not.
|
|
*/
|
|
public function addColumn(string $table, string $column, string $attributes): bool
|
|
{
|
|
$table = $this->_prefix . $table;
|
|
$sql = "ALTER TABLE {$table} ADD {$column} {$attributes}";
|
|
|
|
return !$this->query($sql)->error();
|
|
}
|
|
|
|
/**
|
|
* Convert an array of where clause data into a MySQL WHERE clause and params.
|
|
*
|
|
* @param array $clauses An array, or nested array, of
|
|
* column, operator (default =), value, and glue (default AND).
|
|
* @return array The where clause string, and parameters to bind.
|
|
*/
|
|
public static function makeWhere(array $clauses): array
|
|
{
|
|
if (count($clauses) === count($clauses, COUNT_RECURSIVE)) {
|
|
return self::makeWhere([$clauses]);
|
|
}
|
|
|
|
$where_clauses = [];
|
|
foreach ($clauses as $clause) {
|
|
if (!is_array($clause)) {
|
|
throw new InvalidArgumentException('Where clause must be an array');
|
|
}
|
|
|
|
if (count($clause) !== count($clause, COUNT_RECURSIVE)) {
|
|
self::makeWhere(...$clause);
|
|
continue;
|
|
}
|
|
|
|
$column = null;
|
|
$operator = '=';
|
|
$value = null;
|
|
$glue = 'AND';
|
|
|
|
switch (count($clause)) {
|
|
case 4:
|
|
[$column, $operator, $value, $glue] = $clause;
|
|
break;
|
|
case 3:
|
|
[$column, $operator, $value] = $clause;
|
|
break;
|
|
case 2:
|
|
[$column, $value] = $clause;
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException('Invalid where clause');
|
|
}
|
|
|
|
if (!in_array($operator, ['=', '<>', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE'])) {
|
|
throw new InvalidArgumentException("Invalid operator: {$operator}");
|
|
}
|
|
|
|
$where_clauses[] = [
|
|
'column' => $column,
|
|
'operator' => $operator,
|
|
'value' => $value,
|
|
'glue' => $glue,
|
|
];
|
|
}
|
|
|
|
$first = true;
|
|
$where = '';
|
|
$params = [];
|
|
foreach ($where_clauses as $clause) {
|
|
if ($first) {
|
|
$where .= 'WHERE ';
|
|
$first = false;
|
|
} else {
|
|
$where .= " {$clause['glue']} ";
|
|
}
|
|
|
|
$where .= "`{$clause['column']}` {$clause['operator']} ?";
|
|
$params[] = $clause['value'];
|
|
}
|
|
|
|
return [$where, $params];
|
|
}
|
|
}
|