index/src/Data/MariaDB/MariaDBConnection.php

402 lines
13 KiB
PHP

<?php
// MariaDBConnection.php
// Created: 2021-04-30
// Updated: 2022-02-27
namespace Index\Data\MariaDB;
use mysqli;
use mysqli_sql_exception;
use InvalidArgumentException;
use RuntimeException;
use Index\AString;
use Index\Version;
use Index\Data\BeginTransactionFailedException;
use Index\Data\DataException;
use Index\Data\IDbConnection;
use Index\Data\IDbTransactions;
use Index\Data\CommitFailedException;
use Index\Data\ConnectionFailedException;
use Index\Data\ReleaseSavePointFailedException;
use Index\Data\RollbackFailedException;
use Index\Data\SavePointFailedException;
use Index\Data\QueryExecuteException;
/**
* Represents a connection with a MariaDB or MySQL database server.
*/
class MariaDBConnection implements IDbConnection, IDbTransactions {
/**
* Refresh grant tables.
*
* @var int
*/
public const REFRESH_GRANT = MYSQLI_REFRESH_GRANT;
/**
* Flushes logs, like the FLUSH LOGS; statement.
*
* @var int
*/
public const REFRESH_LOG = MYSQLI_REFRESH_LOG;
/**
* Refresh tables, like the FLUSH TABLES; statement.
*
* @var int
*/
public const REFRESH_TABLES = MYSQLI_REFRESH_TABLES;
/**
* Refreshes the hosts cache, like the FLUSH HOSTS; statement.
*
* @var int
*/
public const REFRESH_HOSTS = MYSQLI_REFRESH_HOSTS;
/**
* Refresh the status variables, like the FLUSH STATUS; statement.
*
* @var int
*/
public const REFRESH_STATUS = MYSQLI_REFRESH_STATUS;
/**
* Flushes the thread cache.
*
* @var int
*/
public const REFRESH_THREADS = MYSQLI_REFRESH_THREADS;
/**
* Resets information about the master server and restarts on slave replication servers,
* like the RESET SLAVE; statement.
*
* @var int
*/
public const REFRESH_SLAVE = MYSQLI_REFRESH_SLAVE;
/**
* Removes binary log files listed in the binary log index and truncates the index file
* on master replication servers, like the RESET MASTER; statement.
*/
public const REFRESH_MASTER = MYSQLI_REFRESH_MASTER;
private mysqli $connection;
/**
* Creates a new instance of MariaDBConnection.
*
* @param MariaDBConnectionInfo $connectionInfo Information about the connection.
* @return MariaDBConnection A new instance of MariaDBConnection.
*/
public function __construct(MariaDBConnectionInfo $connectionInfo) {
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
// I'm not sure if calling "new mysqli" without arguments is equivalent to this
// the documentation would suggest it's not and that it just pulls from the config
// nothing suggests otherwise too.
// The output of mysqli_init is just an object anyway so we can safely use it instead
// but continue to use it as an object.
$this->connection = mysqli_init();
$this->connection->options(MYSQLI_OPT_LOCAL_INFILE, 0);
if($connectionInfo->hasCharacterSet())
$this->connection->options(MYSQLI_SET_CHARSET_NAME, $connectionInfo->getCharacterSet());
if($connectionInfo->hasInitCommand())
$this->connection->options(MYSQLI_INIT_COMMAND, $connectionInfo->getInitCommand());
$flags = $connectionInfo->shouldUseCompression() ? MYSQLI_CLIENT_COMPRESS : 0;
if($connectionInfo->isSecure()) {
$flags |= MYSQLI_CLIENT_SSL;
if($connectionInfo->shouldVerifyCertificate())
$this->connection->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
else
$flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
$this->connection->ssl_set(
$connectionInfo->getKeyPath(),
$connectionInfo->getCertificatePath(),
$connectionInfo->getCertificateAuthorityPath(),
$connectionInfo->getTrustedCertificatesPath(),
$connectionInfo->getCipherAlgorithms()
);
}
try {
if($connectionInfo->isUnixSocket())
$this->connection->real_connect(
'',
$connectionInfo->getUserName(),
$connectionInfo->getPassword(),
$connectionInfo->getDatabaseName(),
-1,
$connectionInfo->getSocketPath(),
$flags
);
else
$this->connection->real_connect(
$connectionInfo->getHost(),
$connectionInfo->getUserName(),
$connectionInfo->getPassword(),
$connectionInfo->getDatabaseName(),
$connectionInfo->getPort(),
'',
$flags
);
} catch(mysqli_sql_exception $ex) {
throw new ConnectionFailedException($ex->getMessage(), $ex->getCode(), $ex);
}
}
/**
* Gets the number of rows affected by the last operation.
*
* @return int|string Number of rows affected by the last operation.
*/
public function getAffectedRows(): int|string {
return $this->connection->affected_rows;
}
/**
* Gets the name of the currently active character set.
*
* @return string Name of the character set.
*/
public function getCharacterSet(): string {
return $this->connection->character_set_name();
}
/**
* Switch to a different character set.
*
* @param string $charSet Name of the new character set.
* @throws InvalidArgumentException Switching to new character set failed.
*/
public function setCharacterSet(string $charSet): void {
if(!$this->connection->set_charset($charSet))
throw new InvalidArgumentException('$charSet is not a supported character set.');
}
/**
* Returns info about the currently character set and collation.
*
* @return MariaDBCharacterSetInfo Information about the character set.
*/
public function getCharacterSetInfo(): MariaDBCharacterSetInfo {
return new MariaDBCharacterSetInfo($this->connection->get_charset());
}
/**
* Switches the connection to a different user and default database.
*
* @param string $userName New user name.
* @param string $password New password.
* @param string|null $database New default database.
* @throws DataException If the user switch action failed.
*/
public function switchUser(string $userName, string $password, string|null $database = null): void {
if(!$this->connection->change_user($userName, $password, $database === null ? null : $database))
throw new DataException($this->getLastErrorString(), $this->getLastErrorCode());
}
/**
* Switches the default database for this connection.
*
* @param string $database New default database.
* @throws DataException If the database switch failed.
*/
public function switchDatabase(string $database): void {
if(!$this->connection->select_db($database))
throw new DataException($this->getLastErrorString(), $this->getLastErrorCode());
}
/**
* Gets the version of the server.
*
* @return Version Version of the server.
*/
public function getServerVersion(): Version {
return MariaDBBackend::intToVersion($this->connection->server_version);
}
/**
* Gets the last error code.
*
* @return int Last error code.
*/
public function getLastErrorCode(): int {
return $this->connection->errno;
}
/**
* Gets the last error string.
*
* @return string Last error string.
*/
public function getLastErrorString(): string {
return $this->connection->error;
}
/**
* Gets the current SQL State of the connection.
*
* @return string Current SQL State.
*/
public function getSQLState(): string {
return $this->connection->sqlstate;
}
/**
* Gets a list of errors from the last command.
*
* @return array List of last errors.
*/
public function getLastErrors(): array {
return MariaDBWarning::fromLastErrors($this->connection->error_list);
}
/**
* Gets the number of columns for the last query.
*
* @return int
*/
public function getLastFieldCount(): int {
return $this->connection->field_count;
}
/**
* Gets list of warnings.
*
* The result of SHOW WARNINGS;
*
* @return array List of warnings.
*/
public function getWarnings(): array {
return MariaDBWarning::fromGetWarnings($this->connection->get_warnings());
}
/**
* Returns the number of warnings for the last query.
*
* @return int Amount of warnings.
*/
public function getWarningCount(): int {
return $this->connection->warning_count;
}
public function getLastInsertId(): int|string {
return $this->connection->insert_id;
}
/**
* Sends a keep alive request to the server, or tries to reconnect if it has gone down.
*
* @return bool true if the keep alive was successful, false if the server is Gone.
*/
public function ping(): bool {
return $this->connection->ping();
}
/**
* Runs various refresh operations, see the REFRESH_ constants for the flags.
*
* @param int $flags A bitset of REFRESH_ flags.
*/
public function refresh(int $flags): void {
$this->connection->refresh($flags);
}
public function setAutoCommit(bool $state): void {
$this->connection->autocommit($state);
}
public function beginTransaction(): void {
if(!$this->connection->begin_transaction())
throw new BeginTransactionFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function commit(): void {
if(!$this->connection->commit())
throw new CommitFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function rollback(?string $name = null): void {
if(!$this->connection->rollback(0, $name))
throw new RollbackFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function savePoint(string $name): void {
if(!$this->connection->savepoint($name))
throw new SavePointFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function releaseSavePoint(string $name): void {
if(!$this->connection->release_savepoint($name))
throw new ReleaseSavePointFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
/**
* Gets the thread ID for the current connection.
*
* @return int Thread ID.
*/
public function getThreadId(): int {
return $this->connection->thread_id;
}
/**
* Asks the server to kill a thread.
*
* @param int $threadId ID of the thread that should be killed.
* @return bool true if the thread was killed, false if not.
*/
public function killThread(int $threadId): bool {
return $this->connection->kill($threadId);
}
/**
* @return MariaDBStatement A database statement.
*/
public function prepare(string $query): MariaDBStatement {
$statement = $this->connection->prepare($query);
if($statement === false)
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
return new MariaDBStatement($statement);
}
/**
* @return MariaDBResult A database result.
*/
public function query(string $query): MariaDBResult {
$result = $this->connection->query($query, MYSQLI_STORE_RESULT);
if($result === false)
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
// Yes, this always uses Native, for some reason the stupid limitation in libmysql only applies to preparing
return new MariaDBResultNative($result);
}
public function execute(string $query): int|string {
if(!$this->connection->real_query($query))
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
return $this->connection->affected_rows;
}
/**
* Closes the connection and associated resources.
*/
public function close(): void {
$this->connection->close();
}
/**
* Closes the connection and associated resources.
*/
public function __destruct() {
$this->close();
}
}