refactor: add chainable methods

This commit is contained in:
Victor Westerlund 2024-06-18 11:20:23 +02:00
parent 302d0cbad0
commit cecaae74a9
3 changed files with 305 additions and 83 deletions

48
.gitignore vendored Normal file
View file

@ -0,0 +1,48 @@
# Bootstrapping #
#################
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
.directory
# Tool specific files #
#######################
# vim
*~
*.swp
*.swo
# sublime text & textmate
*.sublime-*
*.stTheme.cache
*.tmlanguage.cache
*.tmPreferences.cache
# Eclipse
.settings/*
# JetBrains, aka PHPStorm, IntelliJ IDEA
.idea/*
# NetBeans
nbproject/*
# Visual Studio Code
.vscode
# Sass preprocessor
.sass-cache/

53
src/DatabaseDriver.php Normal file
View file

@ -0,0 +1,53 @@
<?php
namespace libsqlitedriver\Driver;
use \SQLite3;
use \SQLite3Result;
class DatabaseDriver extends SQLite3 {
public function __construct(private string $database) {
parent::__construct($database);
}
// Execute a prepared statement and SQLite3Result object
private function run_query(string $query, mixed $values = []): SQLite3Result|bool {
$statement = $this->prepare($query);
// Format optional placeholder "?" with values
if (!empty($values)) {
// Move single arguemnt into array
if (!is_array($values)) {
$values = [$values];
}
foreach ($values as $k => $value) {
$statement->bindValue($k + 1, $value); // Index starts at 1
}
}
// Return SQLite3Result object
return $statement->execute();
}
/* ---- */
// Return rows as assoc array
#[\ReturnTypeWillChange]
public function exec(string $sql, mixed $params = null): array {
$results = [];
$query = $this->run_query($sql, $params);
while ($result = $query->fetchArray(SQLITE3_ASSOC)) {
$results[] = $result;
}
return $results;
}
// Returns true if rows were returned
public function exec_bool(string $sql, mixed $params = null): bool {
$query = $this->run_query($sql, $params);
return $query->numColumns() > 0;
}
}

View file

@ -1,119 +1,240 @@
<?php
namespace libsqlitedriver;
namespace libsqlitedriver;
class SQLite extends \SQLite3 {
function __construct(string $db = ":memory:", string $init = null) {
$this->db_path = $db;
use \Exception;
use \victorwesterlund\xEnum;
// Run .sql file on first run of persistant db
$run_init = false;
use libsqlitedriver\Driver\DatabaseDriver;
// Set path to persistant db
if ($this->db_path !== ":memory:") {
// Get path to database without filename
$path = explode("/", $this->db_path);
array_pop($path);
$path = implode("/", $path);
require_once "DatabaseDriver.php";
// Check write permissions of database
if (!is_writeable($path)) {
throw new \Error("Permission denied: Can not write to directory '{$path}'");
}
// Interface for MySQL_Driver with abstractions for data manipulation
class SQLite extends DatabaseDriver {
private string $table;
private ?array $model = null;
// Database doesn't exist and an init file as been provided
$run_init = !file_exists($db) && $init ? true : $run_init;
}
private bool $flatten = false;
private ?string $order_by = null;
private ?string $filter_sql = null;
private array $filter_values = [];
private int|string|null $limit = null;
parent::__construct($db);
// Pass constructor arguments to driver
function __construct(string $database) {
parent::__construct($database);
}
if ($run_init) {
$this->init_db($init);
private function throw_if_no_table() {
if (!$this->table) {
throw new Exception("No table name defined");
}
}
// Execute a prepared statement and SQLite3Result object
private function run_query(string $query, mixed $values = []): \SQLite3Result|bool {
$statement = $this->prepare($query);
// Format optional placeholder "?" with values
if (!empty($values)) {
// Move single arguemnt into array
if (!is_array($values)) {
$values = [$values];
}
foreach ($values as $k => $value) {
$statement->bindValue($k + 1, $value); // Index starts at 1
}
}
// Return SQLite3Result object
return $statement->execute();
}
// Execute SQL from a file
private function exec_file(string $file): bool {
return $this->exec(file_get_contents($file));
// Return value(s) that exist in $this->model
private function in_model(string|array $columns): ?array {
// Place string into array
$columns = is_array($columns) ? $columns : [$columns];
// Return columns that exist in table model
return array_filter($columns, fn($col): string => in_array($col, $this->model));
}
/* ---- */
// Create comma separated list (CSV) from array
private static function csv(array $values): string {
return implode(",", $values);
// Use the following table name
public function for(string $table): self {
$this->table = $table;
return $this;
}
// Create CSV from columns
public static function columns(array|string $columns): string {
return is_array($columns)
? (__CLASS__)::csv($columns)
: $columns;
// Restrict query to array of column names
public function with(?array $model = null): self {
// Remove table model if empty
if (!$model) {
$this->model = null;
return $this;
}
// Reset table model
$this->model = [];
foreach ($model as $k => $v) {
// Column values must be strings
if (!is_string($v)) {
throw new Exception("Key {$k} must have a value of type string");
}
// Append column to model
$this->model[] = $v;
}
return $this;
}
// Return CSV of '?' for use with prepared statements
public static function values(array|string $values): string {
return is_array($values)
? (__CLASS__)::csv(array_fill(0, count($values), "?"))
: "?";
// Create a WHERE statement from filters
public function where(array ...$conditions): self {
$values = [];
$filters = [];
// Group each condition into an AND block
foreach ($conditions as $condition) {
$filter = [];
// Move along if the condition is empty
if (empty($condition)) {
continue;
}
// Create SQL string and append values to array for prepared statement
foreach ($condition as $col => $value) {
if ($this->model && !$this->in_model($col)) {
continue;
}
// Create SQL for prepared statement
$filter[] = "`{$col}` = ?";
// Append value to array with all other values
$values[] = $value;
}
// AND together all conditions into a group
$filters[] = "(" . implode(" AND ", $filter) . ")";
}
// Do nothing if no filters were set
if (empty($filters)) {
return $this;
}
// OR all filter groups
$this->filter_sql = implode(" OR ", $filters);
// Set values property
$this->filter_values = $values;
return $this;
}
// Return SQL LIMIT string from integer or array of [offset => limit]
public function limit(int|array $limit): self {
// Set LIMIT without range directly as integer
if (is_int($limit)) {
$this->limit = $limit;
return $this;
}
// Use array key as LIMIT range start value
$offset = (int) array_keys($limit)[0];
// Use array value as LIMIT range end value
$limit = (int) array_values($limit)[0];
// Set limit as SQL CSV
$this->limit = "{$offset},{$limit}";
return $this;
}
// Flatten returned array to first entity if set
public function flatten(bool $flag = true): self {
$this->flatten = $flag;
return $this;
}
// Return SQL SORT BY string from assoc array of columns and direction
public function order(array $order_by): self {
// Create CSV from columns
$sql = implode(",", array_keys($order_by));
// Create pipe DSV from values
$sql .= " " . implode("|", array_values($order_by));
$this->order_by = $sql;
return $this;
}
/* ---- */
// Get result as column indexed array
public function return_array(string $query, mixed $values = []): array {
$result = $this->run_query($query, $values);
$rows = [];
// Create Prepared Statament for SELECT with optional WHERE filters
public function select(array|string|null $columns = null): array|bool {
$this->throw_if_no_table();
if (is_bool($result)) {
return [];
// Create array of columns from CSV
$columns = is_array($columns) || is_null($columns) ? $columns : explode(",", $columns);
// Filter columns that aren't in the model if defiend
if ($columns && $this->model) {
$columns = $this->in_model($columns);
}
// Get each row from SQLite3Result
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$rows[] = $row;
// Create CSV from columns or default to SQL NULL as a string
$columns_sql = $columns ? implode(",", $columns) : "NULL";
// Create LIMIT statement if argument is defined
$limit_sql = !is_null($this->limit) ? " LIMIT {$this->limit}" : "";
// Create ORDER BY statement if argument is defined
$order_by_sql = !is_null($this->order_by) ? " ORDER BY {$this->order_by}" : "";
// Get array of SQL WHERE string and filter values
$filter_sql = !is_null($this->filter_sql) ? " WHERE {$this->filter_sql}" : "";
// Interpolate components into an SQL SELECT statmenet and execute
$sql = "SELECT {$columns_sql} FROM {$this->table}{$filter_sql}{$order_by_sql}{$limit_sql}";
// No columns were specified, return true if query matched rows
if (!$columns) {
return $this->exec_bool($sql, $this->filter_values);
}
return $rows;
// Return array of matched rows
$exec = $this->exec($sql, $this->filter_values);
// Return array if exec was successful. Return as flattened array if flag is set
return empty($exec) || !$this->flatten ? $exec : $exec[0];
}
// Get only whether a query was sucessful or not
public function return_bool(string $query, mixed $values = []): bool {
$result = $this->run_query($query, $values);
// Create Prepared Statement for UPDATE using PRIMARY KEY as anchor
public function update(array $entity): bool {
$this->throw_if_no_table();
if (is_bool($result)) {
return $result;
// Make constraint for table model if defined
if ($this->model) {
foreach (array_keys($entity) as $col) {
// Throw if column in entity does not exist in defiend table model
if (!in_array($col, $this->model)) {
throw new Exception("Column key '{$col}' does not exist in table model");
}
}
}
// Get first row or return false
$row = $result->fetchArray(SQLITE3_NUM);
return $row !== false ? true : false;
// Create CSV string with Prepared Statement abbreviations from length of fields array.
$changes = array_map(fn($column) => "{$column} = ?", array_keys($entity));
$changes = implode(",", $changes);
// Get array of SQL WHERE string and filter values
$filter_sql = !is_null($this->filter_sql) ? " WHERE {$this->filter_sql}" : "";
$values = array_values($entity);
// Append filter values if defined
if ($this->filter_values) {
array_push($values, ...$this->filter_values);
}
// Interpolate components into an SQL UPDATE statement and execute
$sql = "UPDATE {$this->table} SET {$changes} {$filter_sql}";
return $this->exec_bool($sql, $values);
}
/* ---- */
// Create Prepared Statemt for INSERT
public function insert(array $values): bool {
$this->throw_if_no_table();
// Initialize a fresh database with SQL from file
private function init_db(string $init) {
return $this->exec_file($init);
// A value for each column in table model must be provided
if ($this->model && count($values) !== count($this->model)) {
throw new Exception("Values length does not match columns in model");
}
// Create CSV string with Prepared Statement abbreviatons from length of fields array.
$values_stmt = implode(",", array_fill(0, count($values), "?"));
// Interpolate components into an SQL INSERT statement and execute
$sql = "INSERT INTO {$this->table} VALUES ({$values_stmt})";
return $this->exec_bool($sql, $values);
}
}