Initial commit

This commit is contained in:
Sky Johnson 2024-07-01 13:10:10 -05:00
parent b0fe251d5a
commit 2e524fb7d0
4 changed files with 535 additions and 1 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
test.db
test.db-shm
test.db-wal

364
Database.php Normal file
View File

@ -0,0 +1,364 @@
<?php
/**
* DBLite wrapper class. Database provides a non-static interface to interact with a
* SQLite database in as simple a way as possible.
*
* The DBLite wrapper provides a simple-to-use database API using PDO's SQLite driver.
* It was designed to have method signatures very close to Laravel's Eloquent ORM. There
* are not, however, any ORM features in the DBLite wrapper. These must be handled at
* the model level.
*
* By default, foreign keys are enabled and the database is in WAL mode. In general
* these defaults should be perfect for most use cases.
*/
class Database
{
/**
* The PDO handle.
*/
private PDO $c;
/**
* The current working table.
*/
private string $table = '';
/**
* The number of queries executed.
*/
private int $queries = 0;
/**
* The log of executed queries.
*/
private array $log = [];
/**
* The last error.
*/
private string $error = '';
/**
* The array for the query builder.
*/
private array $builder = [
'where' => [],
'order' => [],
'limit' => 0,
'values' => [],
];
/**
* Open a connection to the database.
*/
public function __construct(string $path, array $opts = [])
{
$opts = !empty($opts) ? $opts : [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
try {
$this->c = new PDO("sqlite:$path", null, null, $opts);
$this->c->exec('PRAGMA foreign_keys = ON;'); // Enable foreign keys
$this->c->exec('PRAGMA journal_mode = WAL;'); // Enable WAL
} catch (PDOException $e) {
$this->error = "Failed to open database: " . $e->getMessage();
throw $e;
}
}
/**
* Change the current working table.
*/
public function table(string $name): Database
{
$this->table = $name;
return $this;
}
/**
* Create a table with the name of the current working table. Will drop
* the table if it already exists.
*/
public function create(array $columns): PDOStatement|false
{
$query = "CREATE TABLE IF NOT EXISTS $this->table (" . implode(', ', $columns) . ');';
$this->addToLog($query);
return $this->c->query($query);
}
/**
* Drop the current working table if it exists.
*/
public function drop(): PDOStatement|false
{
$query = "DROP TABLE IF EXISTS $this->table;";
$this->addToLog($query);
return $this->c->query($query);
}
/**
* Return the current working table's schema.
*/
public function schema(): array
{
$query = "PRAGMA table_info($this->table);";
$this->addToLog($query);
return $this->c->query($query)->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Insert data into the current working table. Pass an array of key/value
* pairs to insert one record, or an array of arrays to insert multiple. Returns
* the number of rows inserted, or false on failure.
*/
public function insert(array $data): int|false
{
// If the first element is not an array we can assume we're doing a single insert.
if (!isset($data[0])) {
$query = "INSERT INTO $this->table (" . implode(', ', array_keys($data)) . ')'
. ' VALUES (' . implode(', ', array_fill(0, count($data), '?')) . ');';
$this->addToLog($query);
return $this->c->prepare($query)->execute(array_values($data));
}
// Otherwise we will build a multi-insert query.
$query = "INSERT INTO $this->table (" . implode(', ', array_keys($data[0])) . ') VALUES ';
$placeholders = [];
$values = [];
foreach ($data as $row) {
$placeholders[] = '(' . implode(', ', array_fill(0, count($row), '?')) . ')';
foreach ($row as $value) $values[] = $value;
}
$query .= implode(', ', $placeholders) . ';';
$this->addToLog($query);
return $this->c->prepare($query)->execute($values);
}
/**
* Add a where clause to the builder. All values are paramaterized.
*/
public function where(string $column, string $operator, mixed $value): Database
{
$this->builder['where'][] = "$column $operator ?";
$this->builder['values'][] = $value;
return $this;
}
/**
* Add an order clause to the builder.
*/
public function order(string $column, string $direction = 'ASC'): Database
{
$this->builder['order'] = "$column $direction";
return $this;
}
/**
* Set a limit in the builder.
*/
public function limit(int $limit): Database
{
$this->builder['limit'] = $limit;
return $this;
}
/**
* Build a select query and return the result. By default selects the entire
* row. Optionally use fetchAll, but there is also the selectAll() alias method
* for this.
*/
public function select(string $what = '*', bool $fetchAll = false): array|false
{
$query = "SELECT $what FROM $this->table";
if (!empty($this->builder['where'])) $query .= " WHERE " . implode(' AND ', $this->builder['where']);
if (!empty($this->builder['order'])) $query .= " ORDER BY " . $this->builder['order'];
if (!empty($this->builder['limit'])) $query .= " LIMIT " . $this->builder['limit'];
$this->addToLog($query);
try {
$stmt = $this->c->prepare($query);
$stmt->execute($this->builder['values']);
} catch (PDOException $e) {
$this->error = "Failed to execute query: " . $e->getMessage();
return false;
}
$this->resetBuilder();
return $fetchAll ? $stmt->fetchAll() : $stmt->fetch();
}
/**
* Delete records from the current working table. Returns the number of rows
* deleted, or false on failure. Uses where clauses if set.
*/
public function delete(): int|false
{
$query = "DELETE FROM $this->table";
if (!empty($this->builder['where'])) $query .= " WHERE " . implode(' AND ', $this->builder['where']);
$this->addToLog($query);
$this->resetBuilder();
return $this->c->exec($query);
}
/**
* Update records in the current working table. Returns the number of rows
* updated, or false on failure. Uses where clauses if set.
*/
public function update(array $data): int|false
{
$query = "UPDATE $this->table SET " . implode(', ', array_keys($data)) . ' = ?';
if (!empty($this->builder['where'])) $query .= " WHERE " . implode(' AND ', $this->builder['where']);
$this->addToLog($query);
$values = array_merge(array_values($data), $this->builder['values']);
$this->resetBuilder();
return $this->c->prepare($query)->execute($values);
}
/**
* Get the number of rows in the current working table. Can use where clauses.
*/
public function count(): int|false
{
return $this->select('COUNT(*)', true)[0]['COUNT(*)'];
}
/**
* Alias for $db->select(true)
*/
public function selectAll(string $what = '*'): array|false
{
return $this->select($what, true);
}
/**
* Return whether the given table exists.
*/
public function tableExists(string $name): bool
{
$query = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='$name';";
$this->addToLog($query);
return $this->c->query($query)->fetchColumn() > 0;
}
/**
* Return the PDO instance; for when more complex operations are needed.
*/
public function c(): PDO
{
return $this->c;
}
/**
* Alias for PDO::prepare
*/
public function prepare(string $query): PDOStatement
{
return $this->c->prepare($query);
}
/**
* Alias for PDO::lastInsertId
*/
public function lastInsertId(): int
{
return $this->c->lastInsertId();
}
/**
* Alias for PDO::exec
*/
public function exec(string $query): int
{
$this->addToLog($query);
return $this->c->exec($query);
}
/**
* Alias for PDO::beginTransaction
*/
public function begin(): bool
{
return $this->c->beginTransaction();
}
/**
* Alias for PDO::commit
*/
public function commit(): bool
{
return $this->c->commit();
}
/**
* Alias for PDO::rollBack
*/
public function rollBack(): bool
{
return $this->c->rollBack();
}
/**
* Alias for PDO::query
*/
public function query(string $query): PDOStatement|false
{
$this->addToLog($query);
return $this->c->query($query);
}
/**
* Add the query to the log and increment the number of queries executed.
*/
private function addToLog(string $query): void
{
$this->log[] = $query;
$this->queries++;
}
/**
* Reset the builder to it's default structure.
*/
public function resetBuilder(): void
{
$this->builder = [
'where' => [],
'order' => [],
'limit' => 0,
'values' => [],
];
}
/**
* Return the log of executed queries.
*/
public function log(): array
{
return $this->log;
}
/**
* Return the number of queries executed.
*/
public function queries(): int
{
return $this->queries;
}
/**
* Return the last error message.
*/
public function error(): string
{
return $this->error;
}
}

View File

@ -1,3 +1,42 @@
# DBLite
A simple-to-use database API using PDO's SQLite driver.
The DBLite wrapper provides a simple-to-use database API using PDO's SQLite driver.
It was designed to have method signatures very close to Laravel's Eloquent ORM. There
are not, however, any ORM features in the DBLite wrapper. These must be handled at
the model level.
By default, foreign keys are enabled and the database is in WAL mode. In general
these defaults should be perfect for most use cases.
## Testing
I wrote a simple test file; run it with PHP directly. `php test.php`
This test script covers almost every relevant method in the class.
## Examples
```php
$db = new Database('test.db');
$db->table('test')->create([
'id INTEGER PRIMARY KEY',
'name TEXT NOT NULL',
'age INTEGER DEFAULT 18'
]);
$db->table('test')->insert([
'name' => 'John',
'age' => 32
]);
$db->table('test')->where('name', '=', 'John')->select();
$db->table('test')->where('age', '>', 16)->selectAll();
$db->table('test')->where('name', '=', 'John')->delete();
```
## Notes
`Database->table()` sets the current working table's name; there is no real need to use it for every query.
```php
$db->table('test');
$db->where('name', '=', 'John')->select();
$db->where('age', '>', 16)->selectAll('age');
```

128
test.php Normal file
View File

@ -0,0 +1,128 @@
<?php
require_once 'Database.php';
// Remove the test.db file if it exists
if (file_exists('test.db')) {
unlink('test.db');
neutral('Database removed.');
}
// Open the database
$db = new Database('test.db');
// Make sure the database exists
if (!file_exists('test.db')) {
exit(bad('Failed to open database.'));
} else {
good('Database opened successfully.');
}
// Create a table
$db->table('test')->create([
'id INTEGER PRIMARY KEY',
'name TEXT NOT NULL',
'age INTEGER DEFAULT 18'
]);
// Make sure the table exists
if (!$db->tableExists('test')) {
exit(bad('Failed to create table.'));
} else {
good('Table created successfully.');
}
// Insert some data
$db->table('test')->insert([
'name' => 'John',
'age' => 32
]);
$db->table('test')->insert([
['name' => 'Smith', 'age' => 22],
['name' => 'Jane', 'age' => 18]
]);
// Make sure all three rows exist.
if ($db->table('test')->count() !== 3) {
exit(bad('Failed to insert data.'));
} else {
good('Data inserted successfully.');
}
// Make sure we only count two rows above age 18.
if ($db->table('test')->where('age', '>', 18)->count() !== 2) {
exit(bad('Failed to count data.'));
} else {
good('Data counted successfully.');
}
// Delete John from the table.
$db->table('test')->where('name', '=', 'John')->delete();
// Make sure John does not exist, and Jane and Smith still exist.
if (
$db->table('test')->where('name', '=', 'John')->select() == false
&& $db->table('test')->where('name', '=', 'Jane')->select() != false
&& $db->table('test')->where('name', '=', 'Smith')->select() != false
) {
exit(bad('Failed to delete data.'));
} else {
good('Data deleted successfully.');
}
// Make sure that Jane's age is 18.
if ($db->table('test')->where('name', '=', 'Jane')->select()['age'] !== 18) {
exit(bad('Jane is not 18.'));
} else {
good('Jane is 18.');
}
// Update Jane's age to 20.
$db->table('test')->where('name', '=', 'Jane')->update(['age' => 20]);
// Make sure that Jane's age is 20.
if ($db->table('test')->where('name', '=', 'Jane')->select()['age'] !== 20) {
exit(bad('Failed to update Jane\'s age.'));
} else {
good('Jane\'s age updated successfully.');
}
// Drop the table.
$db->table('test')->drop();
// Make sure the table does not exist.
if ($db->tableExists('test')) {
exit(bad('Failed to drop table.'));
} else {
good('Table dropped successfully.');
}
// Delete the test database.
if (file_exists('test.db')) {
unlink('test.db');
file_exists('test.db-shm') && unlink('test.db-shm');
file_exists('test.db-wal') && unlink('test.db-wal');
neutral('Database removed.');
} else {
neutral('Database not found.');
}
// ----------------------------------------------------------------
// ----------------------------------------------------------------
// Helpers
function neutral(string $message)
{
echo "$message" . PHP_EOL;
}
function good(string $message)
{
echo "🟢 $message" . PHP_EOL;
}
function bad(string $message)
{
echo "🔴 $message" . PHP_EOL;
}