mirror of
https://codeberg.org/vlw/php-sqlite.git
synced 2025-09-14 04:43:42 +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;
|
||||
}
|
||||
}
|
287
src/SQLite.php
287
src/SQLite.php
|
@ -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}'");
|
||||
}
|
||||
|
||||
// Database doesn't exist and an init file as been provided
|
||||
$run_init = !file_exists($db) && $init ? true : $run_init;
|
||||
}
|
||||
|
||||
parent::__construct($db);
|
||||
// Interface for MySQL_Driver with abstractions for data manipulation
|
||||
class SQLite extends DatabaseDriver {
|
||||
private string $table;
|
||||
private ?array $model = null;
|
||||
|
||||
if ($run_init) {
|
||||
$this->init_db($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;
|
||||
|
||||
// Pass constructor arguments to driver
|
||||
function __construct(string $database) {
|
||||
parent::__construct($database);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue