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 from(string $table): self { // Reset all definers when a new query begins $this->where(); $this->limit(); $this->order(); $this->table = $table; return $this; } #[\Deprecated( message: "use MySQL->from() instead", since: "3.5.7", )] public function for(string $table): self { $this->from($table); } // 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 $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)); } }