174 lines
6.6 KiB
PHP
174 lines
6.6 KiB
PHP
<?php
|
|
// DbTools.php
|
|
// Created: 2021-05-02
|
|
// Updated: 2024-04-10
|
|
|
|
namespace Index\Data;
|
|
|
|
use Countable;
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* Common database actions.
|
|
*
|
|
* DSN documentation:
|
|
*
|
|
* DbTools only handles the scheme part of the URL,
|
|
* the rest of the URL is described in the documentation of the parseDsn implementation of the respective backend.
|
|
*
|
|
* The scheme can be a PHP classpath to an implementation of IDbBackend, or any of these aliases:
|
|
* - `null`: maps to `NullDb\NullDbBackend` or `Index-Data-NullDb-NullDbBackend`, provides a fallback blackhole database backend.
|
|
* - `mariadb`, `mysql`: maps to `MariaDB\MariaDBBackend` or `Index-Data-MariaDB-MariaDBBackend`, provides a backend based on the mysqli extension.
|
|
* - `sqlite`, `sqlite3`: maps to `SQLite\SQLiteBackend` or `Index-Data-SQLite-SQLiteBackend`, provides a backend based on the sqlite3 extension.
|
|
*
|
|
* Short names are currently hardcoded and cannot be expanded.
|
|
*/
|
|
final class DbTools {
|
|
private const DB_PROTOS = [
|
|
'null' => NullDb\NullDbBackend::class,
|
|
'mariadb' => MariaDB\MariaDBBackend::class,
|
|
'mysql' => MariaDB\MariaDBBackend::class,
|
|
'sqlite' => SQLite\SQLiteBackend::class,
|
|
'sqlite3' => SQLite\SQLiteBackend::class,
|
|
];
|
|
|
|
private static function parseDsnUri(string $dsn): array {
|
|
$uri = parse_url($dsn);
|
|
if($uri === false)
|
|
throw new InvalidArgumentException('$dsn is not a valid uri.');
|
|
return $uri;
|
|
}
|
|
|
|
private static function resolveBackend(array $uri): IDbBackend {
|
|
static $backends = [];
|
|
|
|
$scheme = $uri['scheme'];
|
|
|
|
if(in_array($scheme, $backends))
|
|
$backend = $backends[$scheme];
|
|
else {
|
|
$backend = null;
|
|
|
|
if(array_key_exists($scheme, self::DB_PROTOS))
|
|
$name = self::DB_PROTOS[$scheme];
|
|
else
|
|
$name = str_replace('-', '\\', $scheme);
|
|
|
|
if(class_exists($name) && is_subclass_of($name, IDbBackend::class)) {
|
|
$backend = new $name;
|
|
$name = get_class($backend);
|
|
}
|
|
|
|
if($backend === null)
|
|
throw new DataException('No implementation is available for the specified scheme.');
|
|
if(!$backend->isAvailable())
|
|
throw new DataException('Requested database backend is not available, likely due to missing dependencies.');
|
|
|
|
$backends[$name] = $backend;
|
|
}
|
|
|
|
return $backend;
|
|
}
|
|
|
|
/**
|
|
* Retrieves database backend based on DSN URL.
|
|
*
|
|
* Format of the DSN URLs are described in the documentation of the DbTools class as a whole.
|
|
*
|
|
* @param string $dsn URL to create database connection from.
|
|
* @throws InvalidArgumentException if $dsn is not a valid URL.
|
|
* @throws DataException if no database connection can be made using the URL.
|
|
* @return IDbBackend Database backend instance.
|
|
*/
|
|
public static function backend(string $dsn): IDbBackend {
|
|
return self::resolveBackend(self::parseDsnUri($dsn));
|
|
}
|
|
|
|
/**
|
|
* Parses a DSN URL.
|
|
*
|
|
* Format of the DSN URLs are described in the documentation of the DbTools class as a whole.
|
|
*
|
|
* @param string $dsn URL to create database connection from.
|
|
* @throws InvalidArgumentException if $dsn is not a valid URL.
|
|
* @throws DataException if no database connection can be made using the URL.
|
|
* @return IDbConnectionInfo Database connection info.
|
|
*/
|
|
public static function parse(string $dsn): IDbConnectionInfo {
|
|
$uri = self::parseDsnUri($dsn);
|
|
return self::resolveBackend($uri)->parseDsn($uri);
|
|
}
|
|
|
|
/**
|
|
* Uses a DSN URL to create a database instance.
|
|
*
|
|
* Format of the DSN URLs are described in the documentation of the DbTools class as a whole.
|
|
*
|
|
* @param string $dsn URL to create database connection from.
|
|
* @throws InvalidArgumentException if $dsn is not a valid URL.
|
|
* @throws DataException if no database connection can be made using the URL.
|
|
* @return IDbConnection An active database connection.
|
|
*/
|
|
public static function create(string $dsn): IDbConnection {
|
|
$uri = self::parseDsnUri($dsn);
|
|
$backend = self::resolveBackend($uri);
|
|
return $backend->createConnection($backend->parseDsn($uri));
|
|
}
|
|
|
|
/**
|
|
* Transaction wrapper.
|
|
*
|
|
* Takes a database connection with transaction support and a callable that may return a boolean based on the success of the actions.
|
|
* If the callable returns nothing, nothing will happen.
|
|
* If the callable returns true, commit will be called.
|
|
* If the callable returns false, rollback will be called.
|
|
*
|
|
* @param IDbTransactions $connection A database connection with transaction support.
|
|
* @param callable $callable A callable that handles the transaction, may return a bool.
|
|
*/
|
|
public static function transaction(IDbTransactions $connection, callable $callable): void {
|
|
$connection->beginTransaction();
|
|
$result = $callable($connection) ?? null;
|
|
if(is_bool($result)) {
|
|
if($result)
|
|
$connection->commit();
|
|
else
|
|
$connection->rollback();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detects the DbType of the passed argument. Should be used for DbType::AUTO.
|
|
*
|
|
* @param mixed $value A value of unknown type.
|
|
* @return int DbType of the value passed in the argument.
|
|
*/
|
|
public static function detectType(mixed $value): int {
|
|
if(is_null($value))
|
|
return DbType::NULL;
|
|
if(is_float($value))
|
|
return DbType::FLOAT;
|
|
if(is_int($value))
|
|
return DbType::INTEGER;
|
|
// ┌ should probably also check for Stringable, length should also be taken into consideration
|
|
// ↓ though maybe with that it's better to assume that when an object is passed it'll always be Massive
|
|
if(is_string($value))
|
|
return DbType::STRING;
|
|
return DbType::BLOB;
|
|
}
|
|
|
|
/**
|
|
* Constructs a partial query for prepared statements of lists.
|
|
*
|
|
* @param Countable|array|int $count Amount of times to repeat the string in the $repeat parameter.
|
|
* @param string $repeat String to repeat.
|
|
* @param string $glue Glue character.
|
|
* @return string Glued string ready for being thrown into your prepared statement.
|
|
*/
|
|
public static function prepareListString(Countable|array|int $count, string $repeat = '?', string $glue = ', '): string {
|
|
if(is_countable($count))
|
|
$count = count($count);
|
|
return implode($glue, array_fill(0, $count, $repeat));
|
|
}
|
|
}
|