php-mysql/src/MySQL.php

282 lines
8.7 KiB
PHP

<?php
namespace vlw\MySQL;
use Exception;
use mysqli;
use mysqli_stmt;
use mysqli_result;
use vlw\MySQL\Order;
use vlw\MySQL\Operators;
require_once "Order.php";
require_once "Operators.php";
// Interface for MySQL_Driver with abstractions for data manipulation
class MySQL extends mysqli {
public ?array $columns = null;
protected string $table;
protected ?string $limit = null;
protected ?string $order_by = null;
protected array $filter_columns = [];
protected array $filter_values = [];
protected ?string $filter_sql = null;
// Pass constructor arguments to driver
function __construct() {
parent::__construct(...func_get_args());
}
/*
# Helper methods
*/
private function throw_if_no_table() {
if (!$this->table) {
throw new Exception("No table name defined");
}
}
// Coerce input to single dimensional array
private static function to_list_array(mixed $input): array {
return array_values(is_array($input) ? $input : [$input]);
}
// Convert value to MySQL tinyint
private static function filter_boolean(mixed $value): int {
return (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
// Convert all boolean type values to tinyints in array
private static function filter_booleans(array $values): array {
return array_map(fn(mixed $v): mixed => gettype($v) === "boolean" ? self::filter_boolean($v) : $v, $values);
}
private static function array_wrap_accents(array $input): array {
return array_map(fn(mixed $v): string => "`{$v}`", $input);
}
/*
# Definers
These methods are used to build an SQL query by chaining methods together.
Defined parameters will then be executed by an Executer method.
*/
// Use the following table name
public function for(string $table): self {
// Reset all definers when a new query begins
$this->where();
$this->limit();
$this->order();
$this->table = $table;
return $this;
}
// Create a WHERE statement from filters
public function where(?array ...$conditions): self {
// Unset filters if null was passed
if ($conditions === null) {
$this->filter_sql = null;
$this->filter_columns = null;
$this->filter_values = null;
return $this;
}
$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 => $operation) {
$this->filter_columns[] = $col;
// Assume we want an equals comparison if value is not an array
if (!is_array($operation)) {
$operation = [Operators::EQUALS->value => $operation];
}
// Resolve all operator enum values in inner array
foreach ($operation as $operator => $value) {
// Null values have special syntax
if (is_null($value)) {
// Treat anything that isn't an equals operator as falsy
if ($operator !== Operators::EQUALS->value) {
$filter[] = "`{$col}` IS NOT NULL";
continue;
}
$filter[] = "`{$col}` IS NULL";
continue;
}
// Create SQL for prepared statement
$filter[] = "`{$col}` {$operator} ?";
// 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;
}
// SQL LIMIT string
public function limit(?int $limit = null, ?int $offset = null): self {
// Unset row limit if null was passed
if ($limit === null) {
$this->limit = null;
return $this;
}
// Set LIMIT without range directly as integer
if (is_int($limit)) {
$this->limit = $limit;
return $this;
}
// No offset defined, set limit property directly as string
if (is_null($offset)) {
$this->limit = (string) $limit;
return $this;
}
// Set limit and offset as SQL CSV
$this->limit = "{$offset},{$limit}";
return $this;
}
// Return SQL SORT BY string from assoc array of columns and direction
public function order(?array $order_by = null): self {
// Unset row order by if null was passed
if ($order_by === null) {
$this->order_by = null;
return $this;
}
// Assign Order Enum entries from array of arrays<Order|string>
$orders = array_map(fn(Order|string $order): Order => $order instanceof Order ? $order : Order::tryFrom($order), array_values($order_by));
// Create CSV string with Prepared Statement abbreviations from length of fields array.
$sql = array_map(fn(string $column, Order|string $order): string => "`{$column}` " . $order->value, array_keys($order_by), $orders);
$this->order_by = implode(",", $sql);
return $this;
}
/*
# Executors
These methods execute various statements that each return a mysqli_result
*/
// Create Prepared Statament for SELECT with optional WHERE filters
public function select(array|string|null $columns = null): mysqli_result|bool {
$this->throw_if_no_table();
// Create array of columns from CSV
$this->columns = is_array($columns) || is_null($columns) ? $columns : explode(",", $columns);
// Create CSV from columns or default to SQL NULL as a string
$columns_sql = $this->columns ? implode(",", self::array_wrap_accents($this->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}";
// Return mysqli_response of matched rows
return $this->execute_query($sql, self::to_list_array($this->filter_values));
}
// Create Prepared Statement for UPDATE using PRIMARY KEY as anchor
public function update(array $entity): mysqli_result|bool {
$this->throw_if_no_table();
// 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}" : "";
// Get values from entity and convert booleans to tinyint
$values = self::filter_booleans(array_values($entity));
// Append values to filter property if where() was chained
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->execute_query($sql, self::to_list_array($values));
}
// Create Prepared Statemt for INSERT
public function insert(array $values): mysqli_result|bool {
$this->throw_if_no_table();
/*
Use array keys from $values as columns to insert if array is associative.
Treat statement as an all-columns INSERT if the $values array is sequential.
*/
$columns = !array_is_list($values) ? "(" . implode(",", array_keys($values)) . ")" : "";
// Convert booleans to tinyint
$values = self::filter_booleans($values);
// 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}` {$columns} VALUES ({$values_stmt})";
return $this->execute_query($sql, self::to_list_array($values));
}
// Create Prepared Statemente for DELETE with WHERE condition(s)
public function delete(array ...$conditions): mysqli_result|bool {
$this->throw_if_no_table();
// Set DELETE WHERE conditions from arguments
$this->where(...$conditions);
$sql = "DELETE FROM `{$this->table}` WHERE {$this->filter_sql}";
return $this->execute_query($sql, self::to_list_array($this->filter_values));
}
// Execute SQL query with optional prepared statement and return mysqli_result
public function exec(string $sql, mixed $params = null): mysqli_result {
return $this->execute_query($sql, self::to_list_array($params));
}
}