mirror of
https://codeberg.org/vlw/php-sqlite.git
synced 2025-09-14 12:53:41 +02:00
refactor: add chainable methods
This commit is contained in:
parent
302d0cbad0
commit
cecaae74a9
3 changed files with 305 additions and 83 deletions
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal 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
53
src/DatabaseDriver.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
281
src/SQLite.php
281
src/SQLite.php
|
@ -1,119 +1,240 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace libsqlitedriver;
|
namespace libsqlitedriver;
|
||||||
|
|
||||||
class SQLite extends \SQLite3 {
|
use \Exception;
|
||||||
function __construct(string $db = ":memory:", string $init = null) {
|
use \victorwesterlund\xEnum;
|
||||||
$this->db_path = $db;
|
|
||||||
|
|
||||||
// Run .sql file on first run of persistant db
|
use libsqlitedriver\Driver\DatabaseDriver;
|
||||||
$run_init = false;
|
|
||||||
|
|
||||||
// Set path to persistant db
|
require_once "DatabaseDriver.php";
|
||||||
if ($this->db_path !== ":memory:") {
|
|
||||||
// Get path to database without filename
|
|
||||||
$path = explode("/", $this->db_path);
|
|
||||||
array_pop($path);
|
|
||||||
$path = implode("/", $path);
|
|
||||||
|
|
||||||
// Check write permissions of database
|
// Interface for MySQL_Driver with abstractions for data manipulation
|
||||||
if (!is_writeable($path)) {
|
class SQLite extends DatabaseDriver {
|
||||||
throw new \Error("Permission denied: Can not write to directory '{$path}'");
|
private string $table;
|
||||||
}
|
private ?array $model = null;
|
||||||
|
|
||||||
// Database doesn't exist and an init file as been provided
|
private bool $flatten = false;
|
||||||
$run_init = !file_exists($db) && $init ? true : $run_init;
|
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) {
|
private function throw_if_no_table() {
|
||||||
$this->init_db($init);
|
if (!$this->table) {
|
||||||
|
throw new Exception("No table name defined");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute a prepared statement and SQLite3Result object
|
// Return value(s) that exist in $this->model
|
||||||
private function run_query(string $query, mixed $values = []): \SQLite3Result|bool {
|
private function in_model(string|array $columns): ?array {
|
||||||
$statement = $this->prepare($query);
|
// Place string into array
|
||||||
|
$columns = is_array($columns) ? $columns : [$columns];
|
||||||
// Format optional placeholder "?" with values
|
// Return columns that exist in table model
|
||||||
if (!empty($values)) {
|
return array_filter($columns, fn($col): string => in_array($col, $this->model));
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- */
|
/* ---- */
|
||||||
|
|
||||||
// Create comma separated list (CSV) from array
|
// Use the following table name
|
||||||
private static function csv(array $values): string {
|
public function for(string $table): self {
|
||||||
return implode(",", $values);
|
$this->table = $table;
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV from columns
|
// Restrict query to array of column names
|
||||||
public static function columns(array|string $columns): string {
|
public function with(?array $model = null): self {
|
||||||
return is_array($columns)
|
// Remove table model if empty
|
||||||
? (__CLASS__)::csv($columns)
|
if (!$model) {
|
||||||
: $columns;
|
$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
|
// Create a WHERE statement from filters
|
||||||
public static function values(array|string $values): string {
|
public function where(array ...$conditions): self {
|
||||||
return is_array($values)
|
$values = [];
|
||||||
? (__CLASS__)::csv(array_fill(0, count($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
|
// Create Prepared Statament for SELECT with optional WHERE filters
|
||||||
public function return_array(string $query, mixed $values = []): array {
|
public function select(array|string|null $columns = null): array|bool {
|
||||||
$result = $this->run_query($query, $values);
|
$this->throw_if_no_table();
|
||||||
$rows = [];
|
|
||||||
|
|
||||||
if (is_bool($result)) {
|
// Create array of columns from CSV
|
||||||
return [];
|
$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
|
// Create CSV from columns or default to SQL NULL as a string
|
||||||
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
$columns_sql = $columns ? implode(",", $columns) : "NULL";
|
||||||
$rows[] = $row;
|
|
||||||
|
// 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
|
// Create Prepared Statement for UPDATE using PRIMARY KEY as anchor
|
||||||
public function return_bool(string $query, mixed $values = []): bool {
|
public function update(array $entity): bool {
|
||||||
$result = $this->run_query($query, $values);
|
$this->throw_if_no_table();
|
||||||
|
|
||||||
if (is_bool($result)) {
|
// Make constraint for table model if defined
|
||||||
return $result;
|
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
|
// Create CSV string with Prepared Statement abbreviations from length of fields array.
|
||||||
$row = $result->fetchArray(SQLITE3_NUM);
|
$changes = array_map(fn($column) => "{$column} = ?", array_keys($entity));
|
||||||
return $row !== false ? true : false;
|
$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
|
// A value for each column in table model must be provided
|
||||||
private function init_db(string $init) {
|
if ($this->model && count($values) !== count($this->model)) {
|
||||||
return $this->exec_file($init);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue