Compare commits

...

3 commits

33 changed files with 307 additions and 332 deletions

View file

@ -1 +1 @@
0.2410.182054 0.2410.191515

View file

@ -1,7 +1,7 @@
<?php <?php
// ArrayCacheProvider.php // ArrayCacheProvider.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-10-02 // Updated: 2024-10-19
namespace Index\Cache\ArrayCache; namespace Index\Cache\ArrayCache;
@ -74,6 +74,4 @@ class ArrayCacheProvider implements CacheProvider {
public function decrement(string $key, int $amount = 1): int { public function decrement(string $key, int $amount = 1): int {
return $this->increment($key, $amount * -1); return $this->increment($key, $amount * -1);
} }
public function close(): void {}
} }

View file

@ -1,16 +1,14 @@
<?php <?php
// CacheProvider.php // CacheProvider.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-10-02 // Updated: 2024-10-19
namespace Index\Cache; namespace Index\Cache;
use Index\Closeable;
/** /**
* Represents a cache provider. * Represents a cache provider.
*/ */
interface CacheProvider extends Closeable { interface CacheProvider {
/** /**
* Retrieve an item from the cache. * Retrieve an item from the cache.
* *

View file

@ -1,7 +1,7 @@
<?php <?php
// MemcachedProvider.php // MemcachedProvider.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-10-02 // Updated: 2024-10-19
namespace Index\Cache\Memcached; namespace Index\Cache\Memcached;
@ -19,9 +19,4 @@ abstract class MemcachedProvider implements CacheProvider {
public abstract function touch(string $key, int $ttl = 0): void; public abstract function touch(string $key, int $ttl = 0): void;
public abstract function increment(string $key, int $amount = 1): int; public abstract function increment(string $key, int $amount = 1): int;
public abstract function decrement(string $key, int $amount = 1): int; public abstract function decrement(string $key, int $amount = 1): int;
public abstract function close(): void;
public function __destruct() {
$this->close();
}
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// MemcachedProviderLegacy.php // MemcachedProviderLegacy.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-10-02 // Updated: 2024-10-19
namespace Index\Cache\Memcached; namespace Index\Cache\Memcached;
@ -91,7 +91,7 @@ class MemcachedProviderLegacy extends MemcachedProvider {
return $result; return $result;
} }
public function close(): void { public function __destruct() {
if(!$this->persistent) if(!$this->persistent)
$this->memcache->close(); $this->memcache->close();
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// MemcachedProviderModern.php // MemcachedProviderModern.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-10-02 // Updated: 2024-10-19
namespace Index\Cache\Memcached; namespace Index\Cache\Memcached;
@ -111,7 +111,7 @@ class MemcachedProviderModern extends MemcachedProvider {
return $result; return $result;
} }
public function close(): void { public function __destruct() {
if(!$this->memcached->isPersistent()) if(!$this->memcached->isPersistent())
$this->memcached->quit(); $this->memcached->quit();
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// ValkeyProvider.php // ValkeyProvider.php
// Created: 2024-04-10 // Created: 2024-04-10
// Updated: 2024-10-02 // Updated: 2024-10-19
namespace Index\Cache\Valkey; namespace Index\Cache\Valkey;
@ -72,12 +72,8 @@ class ValkeyProvider implements CacheProvider {
return is_int($result) ? $result : 0; return is_int($result) ? $result : 0;
} }
public function close(): void { public function __destruct() {
if(!$this->persist) if(!$this->persist)
$this->redis->close(); $this->redis->close();
} }
public function __destruct() {
$this->close();
}
} }

View file

@ -1,30 +0,0 @@
<?php
// Closeable.php
// Created: 2021-04-30
// Updated: 2024-10-02
namespace Index;
/**
* Provides an interface for releasing unmanaged resources.
*
* If Closeable is implemented __destruct() should also be added to the class and should call close in it:
*
* <code>
* public function close(): void {
* fclose($this->resource);
* }
*
* public function __destruct() {
* $this->close();
* }
* </code>
*
* However if close() is only implemented because a parent interface requires it, the __destruct() implementation may be omitted.
*/
interface Closeable {
/**
* Free, release or reset unmanaged resources.
*/
function close(): void;
}

View file

@ -1,16 +1,16 @@
<?php <?php
// DbConnection.php // DbConnection.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db; namespace Index\Db;
use Index\Closeable; use RuntimeException;
/** /**
* Represents a connection to a database service. * Represents a connection to a database service.
*/ */
interface DbConnection extends Closeable { interface DbConnection {
/** /**
* Returns the ID of the last inserted row. * Returns the ID of the last inserted row.
* *
@ -53,4 +53,11 @@ interface DbConnection extends Closeable {
* @return int|string Number of rows affected by the query. * @return int|string Number of rows affected by the query.
*/ */
function execute(string $query): int|string; function execute(string $query): int|string;
/**
* Begins a transaction.
*
* @throws RuntimeException If beginning the transaction failed.
*/
function beginTransaction(): DbTransaction;
} }

View file

@ -1,16 +1,14 @@
<?php <?php
// DbResult.php // DbResult.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db; namespace Index\Db;
use Index\Closeable;
/** /**
* Represents a database result set. * Represents a database result set.
*/ */
interface DbResult extends Closeable { interface DbResult {
/** /**
* Fetches the next result set. * Fetches the next result set.
* *

View file

@ -1,18 +1,17 @@
<?php <?php
// DbStatement.php // DbStatement.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db; namespace Index\Db;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\Closeable;
/** /**
* Represents a prepared database statement. * Represents a prepared database statement.
*/ */
interface DbStatement extends Closeable { interface DbStatement {
/** /**
* Returns how many parameters there are. * Returns how many parameters there are.
* *
@ -25,7 +24,7 @@ interface DbStatement extends Closeable {
* *
* @param int $ordinal Index of the target parameter. * @param int $ordinal Index of the target parameter.
* @param mixed $value Value to assign to the parameter. * @param mixed $value Value to assign to the parameter.
* @param int $type Type of the value, if left to DbType::AUTO DbTools::detectType will be used on $value. * @param int $type Type of the value, if left to DbType::AUTO DbType::detect will be used on $value.
* @throws InvalidArgumentException If $ordinal exceeds bounds. * @throws InvalidArgumentException If $ordinal exceeds bounds.
*/ */
function addParameter(int $ordinal, mixed $value, int $type = DbType::AUTO): void; function addParameter(int $ordinal, mixed $value, int $type = DbType::AUTO): void;
@ -37,7 +36,7 @@ interface DbStatement extends Closeable {
* Overwriting lower ordinals than the current cursor should be fine, but your mileage may vary. * Overwriting lower ordinals than the current cursor should be fine, but your mileage may vary.
* *
* @param mixed $value Value to assign to the parameter. * @param mixed $value Value to assign to the parameter.
* @param int $type Type of the value, if left to DbType::AUTO DbTools::detectType will be used on $value. * @param int $type Type of the value, if left to DbType::AUTO DbType::detect will be used on $value.
* @throws RuntimeException If all parameters have already been specified. * @throws RuntimeException If all parameters have already been specified.
*/ */
function nextParameter(mixed $value, int $type = DbType::AUTO): void; function nextParameter(mixed $value, int $type = DbType::AUTO): void;

View file

@ -1,7 +1,7 @@
<?php <?php
// DbStatementCache.php // DbStatementCache.php
// Created: 2023-07-21 // Created: 2023-07-21
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db; namespace Index\Db;
@ -54,8 +54,6 @@ class DbStatementCache {
* Closes all statement instances and resets the cache. * Closes all statement instances and resets the cache.
*/ */
public function clear(): void { public function clear(): void {
foreach($this->stmts as $stmt)
$stmt->close();
$this->stmts = []; $this->stmts = [];
} }
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// DbTools.php // DbTools.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-16 // Updated: 2024-10-19
namespace Index\Db; namespace Index\Db;
@ -11,46 +11,6 @@ use Countable;
* Common database actions. * Common database actions.
*/ */
final class DbTools { final class DbTools {
/**
* 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 DbTransactions $connection A database connection with transaction support.
* @param callable $callable A callable that handles the transaction, may return a bool.
*/
public static function transaction(DbTransactions $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;
if(is_resource($value))
return DbType::BLOB;
return DbType::STRING;
}
/** /**
* Constructs a partial query for prepared statements of lists. * Constructs a partial query for prepared statements of lists.
* *

46
src/Db/DbTransaction.php Normal file
View file

@ -0,0 +1,46 @@
<?php
// DbTransaction.php
// Created: 2024-10-19
// Updated: 2024-10-19
namespace Index\Db;
use RuntimeException;
/**
* Represents a transaction to be performed on the database.
*/
interface DbTransaction {
/**
* Commits the actions done during a transaction and ends the transaction.
* A new transaction will be started if auto-commit is disabled.
*
* @throws RuntimeException If the commit failed.
*/
function commit(): void;
/**
* Creates a save point in the transaction that can be rolled back to.
*
* @param string $name Name for the save point.
* @throws RuntimeException If save point creation failed.
*/
function save(string $name): void;
/**
* Releases a specified save point.
*
* @param string $name Name of the save point.
* @throws RuntimeException If save points are not supported by this implementation.
*/
function release(string $name): void;
/**
* Rolls back to the state before a transaction start or to a specified save point.
*
* @param ?string $name Name of the save point, null for the entire transaction.
* @throws RuntimeException If rollback failed.
* @throws RuntimeException If save points are not supported by this implementation and $name was non-null.
*/
function rollback(?string $name = null): void;
}

View file

@ -1,57 +0,0 @@
<?php
// DbTransactions.php
// Created: 2021-05-02
// Updated: 2024-10-04
namespace Index\Db;
/**
* Indicates supports for transactions in a database connection.
*/
interface DbTransactions extends DbConnection {
/**
* Sets whether changes should be applied immediately or whether commit should always be called first.
*
* @param bool $state true if things should automatically be committed, false if not.
*/
function setAutoCommit(bool $state): void;
/**
* Enters a transaction.
*
* @throws \RuntimeException If the creation of the transaction failed.
*/
function beginTransaction(): void;
/**
* Commits the actions done during a transaction and ends the transaction.
* A new transaction will be started if auto-commit is disabled.
*
* @throws \RuntimeException If the commit failed.
*/
function commit(): void;
/**
* Rolls back to the state before a transaction start or to a specified save point.
*
* @param ?string $name Name of the save point, null for the entire transaction.
* @throws \RuntimeException If rollback failed.
*/
function rollback(?string $name = null): void;
/**
* Creates a save point in the transaction that can be rolled back to.
*
* @param string $name Name for the save point.
* @throws \RuntimeException If save point creation failed.
*/
function savePoint(string $name): void;
/**
* Releases a save point.
*
* @param string $name Name of the save point.
* @throws \RuntimeException If releasing the save point failed.
*/
function releaseSavePoint(string $name): void;
}

View file

@ -1,7 +1,7 @@
<?php <?php
// DbType.php // DbType.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db; namespace Index\Db;
@ -10,7 +10,7 @@ namespace Index\Db;
*/ */
final class DbType { final class DbType {
/** /**
* Automatically detect the type. Should be used in combination with DbTools::detectType. * Automatically detect the type. Should be used in combination with DbType::detect.
* *
* @var int * @var int
*/ */
@ -50,4 +50,22 @@ final class DbType {
* @var int * @var int
*/ */
public const BLOB = 5; public const BLOB = 5;
/**
* 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 detect(mixed $value): int {
if(is_null($value))
return self::NULL;
if(is_float($value))
return self::FLOAT;
if(is_int($value))
return self::INTEGER;
if(is_resource($value))
return self::BLOB;
return self::STRING;
}
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDbConnection.php // MariaDbConnection.php
// Created: 2021-04-30 // Created: 2021-04-30
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\MariaDb; namespace Index\Db\MariaDb;
@ -10,12 +10,12 @@ use mysqli_sql_exception;
use stdClass; use stdClass;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\Db\{DbConnection,DbTransactions}; use Index\Db\{DbConnection,DbTransaction};
/** /**
* Represents a connection with a MariaDB or MySQL database server. * Represents a connection with a MariaDB or MySQL database server.
*/ */
class MariaDbConnection implements DbConnection, DbTransactions { class MariaDbConnection implements DbConnection {
/** /**
* Refresh grant tables. * Refresh grant tables.
* *
@ -144,6 +144,8 @@ class MariaDbConnection implements DbConnection, DbTransactions {
} catch(mysqli_sql_exception $ex) { } catch(mysqli_sql_exception $ex) {
throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex); throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex);
} }
$this->connection->autocommit(true);
} }
public function getAffectedRows(): int|string { public function getAffectedRows(): int|string {
@ -189,7 +191,7 @@ class MariaDbConnection implements DbConnection, DbTransactions {
*/ */
public function switchUser(string $userName, string $password, string|null $database = null): void { public function switchUser(string $userName, string $password, string|null $database = null): void {
if(!$this->connection->change_user($userName, $password, $database === null ? null : $database)) if(!$this->connection->change_user($userName, $password, $database === null ? null : $database))
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
} }
/** /**
@ -200,7 +202,7 @@ class MariaDbConnection implements DbConnection, DbTransactions {
*/ */
public function switchDatabase(string $database): void { public function switchDatabase(string $database): void {
if(!$this->connection->select_db($database)) if(!$this->connection->select_db($database))
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
} }
/** /**
@ -230,6 +232,15 @@ class MariaDbConnection implements DbConnection, DbTransactions {
return $this->connection->error; return $this->connection->error;
} }
/**
* Throws the last error as a RuntimeException.
*
* @throws RuntimeException Based on the last error code and string.
*/
public function throwLastError(): never {
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
/** /**
* Gets the current SQL State of the connection. * Gets the current SQL State of the connection.
* *
@ -302,33 +313,11 @@ class MariaDbConnection implements DbConnection, DbTransactions {
$this->connection->refresh($flags); $this->connection->refresh($flags);
} }
public function setAutoCommit(bool $state): void { public function beginTransaction(): DbTransaction {
$this->connection->autocommit($state);
}
public function beginTransaction(): void {
if(!$this->connection->begin_transaction()) if(!$this->connection->begin_transaction())
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
}
public function commit(): void { return new MariaDbTransaction($this, $this->connection);
if(!$this->connection->commit())
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function rollback(?string $name = null): void {
if(!$this->connection->rollback(0, $name))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function savePoint(string $name): void {
if(!$this->connection->savepoint($name))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function releaseSavePoint(string $name): void {
if(!$this->connection->release_savepoint($name))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
} }
/** /**
@ -360,7 +349,7 @@ class MariaDbConnection implements DbConnection, DbTransactions {
throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex); throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex);
} }
if($statement === false) if($statement === false)
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
return new MariaDbStatement($statement); return new MariaDbStatement($statement);
} }
@ -375,7 +364,7 @@ class MariaDbConnection implements DbConnection, DbTransactions {
} }
if($result === false) if($result === false)
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
// the mysql library is very adorable // the mysql library is very adorable
if($result === true) if($result === true)
@ -388,7 +377,7 @@ class MariaDbConnection implements DbConnection, DbTransactions {
public function execute(string $query): int|string { public function execute(string $query): int|string {
try { try {
if(!$this->connection->real_query($query)) if(!$this->connection->real_query($query))
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
} catch(mysqli_sql_exception $ex) { } catch(mysqli_sql_exception $ex) {
throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex); throw new RuntimeException($ex->getMessage(), $ex->getCode(), $ex);
} }
@ -398,16 +387,9 @@ class MariaDbConnection implements DbConnection, DbTransactions {
/** /**
* Closes the connection and associated resources. * Closes the connection and associated resources.
*/ */
public function close(): void { public function __destruct() {
try { try {
$this->connection->close(); $this->connection->close();
} catch(\Error $ex) {} } catch(\Error $ex) {}
} }
/**
* Closes the connection and associated resources.
*/
public function __destruct() {
$this->close();
}
} }

View file

@ -1,11 +1,11 @@
<?php <?php
// MariaDbParameter.php // MariaDbParameter.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\MariaDb; namespace Index\Db\MariaDb;
use Index\Db\{DbTools,DbType}; use Index\Db\DbType;
/** /**
* Represents a bound parameter. * Represents a bound parameter.
@ -25,7 +25,7 @@ class MariaDbParameter {
private mixed $value private mixed $value
) { ) {
if($type == DbType::AUTO) if($type == DbType::AUTO)
$type = DbTools::detectType($value); $type = DbType::detect($value);
if($type === DbType::NULL) if($type === DbType::NULL)
$value = null; $value = null;
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDbResult.php // MariaDbResult.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\MariaDb; namespace Index\Db\MariaDb;
@ -66,10 +66,4 @@ abstract class MariaDbResult implements DbResult {
public function getValue(int|string $index): mixed { public function getValue(int|string $index): mixed {
return $this->currentRow[$index] ?? null; return $this->currentRow[$index] ?? null;
} }
abstract function close(): void;
public function __destruct() {
$this->close();
}
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDbResultLib.php // MariaDbResultLib.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\MariaDb; namespace Index\Db\MariaDb;
@ -54,7 +54,7 @@ class MariaDbResultLib extends MariaDbResult {
return true; return true;
} }
public function close(): void { public function __destruct() {
try { try {
$this->result->free_result(); $this->result->free_result();
} catch(\Error $ex) {} } catch(\Error $ex) {}

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDbResultNative.php // MariaDbResultNative.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\MariaDb; namespace Index\Db\MariaDb;
@ -36,7 +36,7 @@ class MariaDbResultNative extends MariaDbResult {
return true; return true;
} }
public function close(): void { public function __destruct() {
try { try {
$this->result->close(); $this->result->close();
} catch(\Error $ex) {} } catch(\Error $ex) {}

View file

@ -1,7 +1,7 @@
<?php <?php
// MariaDbStatement.php // MariaDbStatement.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\MariaDb; namespace Index\Db\MariaDb;
@ -159,12 +159,8 @@ class MariaDbStatement implements DbStatement {
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode());
} }
public function close(): void {
$this->statement->close();
}
public function __destruct() { public function __destruct() {
$this->close(); $this->statement->close();
} }
} }

View file

@ -0,0 +1,43 @@
<?php
// MariaDbTransaction.php
// Created: 2024-10-19
// Updated: 2024-10-19
namespace Index\Db\MariaDb;
use mysqli;
use Index\Db\DbTransaction;
/**
* Represents a MariaDB transaction.
*/
class MariaDbTransaction implements DbTransaction {
/**
* @param MariaDbConnection $conn Underlying connection
* @param mysqli $connRaw Underlying connection (off vocal)
*/
public function __construct(
private MariaDbConnection $conn,
private mysqli $connRaw
) {}
public function commit(): void {
if(!$this->connRaw->commit())
$this->conn->throwLastError();
}
public function save(string $name): void {
if(!$this->connRaw->savepoint($name))
$this->conn->throwLastError();
}
public function release(string $name): void {
if(!$this->connRaw->release_savepoint($name))
$this->conn->throwLastError();
}
public function rollback(?string $name = null): void {
if(!$this->connRaw->rollback(0, $name))
$this->conn->throwLastError();
}
}

View file

@ -1,11 +1,11 @@
<?php <?php
// NullDbConnection.php // NullDbConnection.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\NullDb; namespace Index\Db\NullDb;
use Index\Db\{DbConnection,DbStatement,DbResult}; use Index\Db\{DbConnection,DbResult,DbStatement,DbTransaction};
/** /**
* Represents a dummy database connection. * Represents a dummy database connection.
@ -31,5 +31,7 @@ class NullDbConnection implements DbConnection {
return 0; return 0;
} }
public function close(): void {} public function beginTransaction(): DbTransaction {
return new NullDbTransaction;
}
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// NullDbResult.php // NullDbResult.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\NullDb; namespace Index\Db\NullDb;
@ -58,6 +58,4 @@ class NullDbResult implements DbResult {
public function getIterator(callable $construct): DbResultIterator { public function getIterator(callable $construct): DbResultIterator {
return new DbResultIterator($this, $construct); return new DbResultIterator($this, $construct);
} }
public function close(): void {}
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// NullDbStatement.php // NullDbStatement.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\NullDb; namespace Index\Db\NullDb;
@ -32,6 +32,4 @@ class NullDbStatement implements DbStatement {
} }
public function reset(): void {} public function reset(): void {}
public function close(): void {}
} }

View file

@ -0,0 +1,20 @@
<?php
// NullDbTransaction.php
// Created: 2024-10-19
// Updated: 2024-10-19
namespace Index\Db\NullDb;
use Index\Db\{DbConnection,DbTransaction};
/**
* Represents a dummy database transaction.
*/
class NullDbTransaction implements DbTransaction {
public function commit(): void {}
public function save(string $name): void {}
public function release(string $name): void {}
public function rollback(?string $name = null): void {}
}

View file

@ -1,18 +1,18 @@
<?php <?php
// SqliteConnection.php // SqliteConnection.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\Sqlite; namespace Index\Db\Sqlite;
use RuntimeException; use RuntimeException;
use SQLite3; use SQLite3;
use Index\Db\{DbConnection,DbTransactions,DbStatement,DbResult}; use Index\Db\{DbConnection,DbResult,DbStatement,DbTransaction};
/** /**
* Represents a client for an SQLite database. * Represents a client for an SQLite database.
*/ */
class SqliteConnection implements DbConnection, DbTransactions { class SqliteConnection implements DbConnection {
/** /**
* CREATE INDEX authorizer. * CREATE INDEX authorizer.
* *
@ -352,7 +352,6 @@ class SqliteConnection implements DbConnection, DbTransactions {
public const IGNORE = SQLite3::IGNORE; public const IGNORE = SQLite3::IGNORE;
private SQLite3 $connection; private SQLite3 $connection;
private bool $autoCommit = true;
/** /**
* Creates a new SQLite client. * Creates a new SQLite client.
@ -456,6 +455,15 @@ class SqliteConnection implements DbConnection, DbTransactions {
return $this->connection->lastErrorCode(); return $this->connection->lastErrorCode();
} }
/**
* Throws the last error as a RuntimeException.
*
* @throws RuntimeException Based on the last error code and string.
*/
public function throwLastError(): never {
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
/** /**
* Opens a BLOB field as a resource. * Opens a BLOB field as a resource.
* *
@ -468,7 +476,7 @@ class SqliteConnection implements DbConnection, DbTransactions {
public function getBlobStream(string $table, string $column, int $rowId): mixed { public function getBlobStream(string $table, string $column, int $rowId): mixed {
$handle = $this->connection->openBlob($table, $column, $rowId); $handle = $this->connection->openBlob($table, $column, $rowId);
if(!is_resource($handle)) if(!is_resource($handle))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
return $handle; return $handle;
} }
@ -480,73 +488,45 @@ class SqliteConnection implements DbConnection, DbTransactions {
public function prepare(string $query): DbStatement { public function prepare(string $query): DbStatement {
$statement = $this->connection->prepare($query); $statement = $this->connection->prepare($query);
if($statement === false) if($statement === false)
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
return new SqliteStatement($this, $statement); return new SqliteStatement($this, $statement);
} }
public function query(string $query): DbResult { public function query(string $query): DbResult {
$result = $this->connection->query($query); $result = $this->connection->query($query);
if($result === false) if($result === false)
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
return new SqliteResult($result); return new SqliteResult($result);
} }
/**
* Tries to execute a query but throws an exception if it fails.
*
* @param string $query Command to execute.
* @throws RuntimeException If the command failed to execute.
*/
public function executeThrow(string $query): void {
if(!$this->connection->exec($query))
$this->throwLastError();
}
public function execute(string $query): int|string { public function execute(string $query): int|string {
if(!$this->connection->exec($query)) if(!$this->connection->exec($query))
throw new RuntimeException($this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
return $this->connection->changes(); return $this->connection->changes();
} }
public function setAutoCommit(bool $state): void { public function beginTransaction(): DbTransaction {
// there's not really a completely reliable way to set a persistent auto commit disable state
if($state === $this->autoCommit)
return;
$this->autoCommit = $state;
if($state) {
$this->commit();
} else {
$this->beginTransaction();
}
}
public function beginTransaction(): void {
if(!$this->connection->exec('BEGIN;')) if(!$this->connection->exec('BEGIN;'))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode()); $this->throwLastError();
return new SqliteTransaction($this);
} }
public function commit(): void { public function __destruct() {
if(!$this->connection->exec('COMMIT;'))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
if(!$this->autoCommit)
$this->beginTransaction();
}
public function rollback(?string $name = null): void {
if(!$this->connection->exec(empty($name) ? 'ROLLBACK;' : "ROLLBACK TO {$name};"))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
if(!$this->autoCommit)
$this->beginTransaction();
}
public function savePoint(string $name): void {
if(!$this->connection->exec("SAVEPOINT {$name};"))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function releaseSavePoint(string $name): void {
if(!$this->connection->exec("RELEASE SAVEPOINT {$name};"))
throw new RuntimeException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function close(): void {
$this->connection->close(); $this->connection->close();
} }
/**
* Closes the connection and associated resources.
*/
public function __destruct() {
$this->close();
}
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// SqliteResult.php // SqliteResult.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\Sqlite; namespace Index\Db\Sqlite;
@ -52,6 +52,4 @@ class SqliteResult implements DbResult {
public function getValue(int|string $index): mixed { public function getValue(int|string $index): mixed {
return $this->currentRow[$index] ?? null; return $this->currentRow[$index] ?? null;
} }
public function close(): void {}
} }

View file

@ -1,7 +1,7 @@
<?php <?php
// SqliteStatement.php // SqliteStatement.php
// Created: 2021-05-02 // Created: 2021-05-02
// Updated: 2024-10-04 // Updated: 2024-10-19
namespace Index\Db\Sqlite; namespace Index\Db\Sqlite;
@ -9,7 +9,7 @@ use SQLite3Result;
use SQLite3Stmt; use SQLite3Stmt;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
use Index\Db\{DbTools,DbType,DbStatement,DbResult}; use Index\Db\{DbType,DbStatement,DbResult};
/** /**
* Represents a prepared SQLite SQL statement. * Represents a prepared SQLite SQL statement.
@ -47,7 +47,7 @@ class SqliteStatement implements DbStatement {
private function bindParameter(int $ordinal, mixed $value, int $type): void { private function bindParameter(int $ordinal, mixed $value, int $type): void {
if($type === DbType::AUTO) if($type === DbType::AUTO)
$type = DbTools::detectType($value); $type = DbType::detect($value);
if($type === DbType::NULL) if($type === DbType::NULL)
$value = null; $value = null;
@ -99,6 +99,4 @@ class SqliteStatement implements DbStatement {
if(!$this->statement->reset()) if(!$this->statement->reset())
throw new RuntimeException((string)$this->connection->getLastErrorString(), $this->connection->getLastErrorCode()); throw new RuntimeException((string)$this->connection->getLastErrorString(), $this->connection->getLastErrorCode());
} }
public function close(): void {}
} }

View file

@ -0,0 +1,41 @@
<?php
// SqliteTransaction.php
// Created: 2024-10-19
// Updated: 2024-10-19
namespace Index\Db\Sqlite;
use SQLite3;
use Index\Db\DbTransaction;
/**
* Represents an SQLite transaction.
*/
class SqliteTransaction implements DbTransaction {
/**
* @param SqliteConnection $conn Underlying connection.
*/
public function __construct(
private SqliteConnection $conn
) {}
public function commit(): void {
$this->conn->executeThrow('COMMIT;');
}
public function save(string $name): void {
$this->conn->executeThrow(sprintf('SAVEPOINT %s;', SQLite3::escapeString($name)));
}
public function release(string $name): void {
$this->conn->executeThrow(sprintf('RELEASE SAVEPOINT %s;', SQLite3::escapeString($name)));
}
public function rollback(?string $name = null): void {
$this->conn->executeThrow(
$name === null
? 'ROLLBACK;'
: sprintf('ROLLBACK TO SAVEPOINT %s;', SQLite3::escapeString($name))
);
}
}

View file

@ -1,30 +0,0 @@
<?php
// DbToolsTest.php
// Created: 2021-04-28
// Updated: 2024-10-16
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use Index\Db\{DbTools,DbType};
#[CoversClass(DbTools::class)]
#[CoversClass(DbType::class)]
final class DbToolsTest extends TestCase {
public function testDetectType(): void {
$this->assertEquals(DbType::NULL, DbTools::detectType(null));
$this->assertEquals(DbType::INTEGER, DbTools::detectType(12345));
$this->assertEquals(DbType::FLOAT, DbTools::detectType(123.45));
$this->assertEquals(DbType::STRING, DbTools::detectType('This is a string.'));
$blob = fopen('php://memory', 'r+b');
if($blob === false)
throw new RuntimeException('failed to fopen a memory stream');
fwrite($blob, 'This is a string inside a memory stream.');
fseek($blob, 0);
$this->assertEquals(DbType::BLOB, DbTools::detectType($blob));
}
}

29
tests/DbTypeTest.php Normal file
View file

@ -0,0 +1,29 @@
<?php
// DbTypeTest.php
// Created: 2021-04-28
// Updated: 2024-10-19
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use Index\Db\DbType;
#[CoversClass(DbType::class)]
final class DbTypeTest extends TestCase {
public function testDetectType(): void {
$this->assertEquals(DbType::NULL, DbType::detect(null));
$this->assertEquals(DbType::INTEGER, DbType::detect(12345));
$this->assertEquals(DbType::FLOAT, DbType::detect(123.45));
$this->assertEquals(DbType::STRING, DbType::detect('This is a string.'));
$blob = fopen('php://memory', 'r+b');
if($blob === false)
throw new RuntimeException('failed to fopen a memory stream');
fwrite($blob, 'This is a string inside a memory stream.');
fseek($blob, 0);
$this->assertEquals(DbType::BLOB, DbType::detect($blob));
}
}