Compare commits

..

28 commits

Author SHA1 Message Date
73297feb82 fix: use isset() when checking table property value (#52)
We can't access the `$this->table` property before initialization, so let's use `isset()` to check if the property has a value.

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/52
2025-08-30 09:49:14 +02:00
0e367f797f fix: only set WHERE filters on delete() if conditions are provided (#51)
This PR fixes a bug where if no conditions are passed to `MySQL->from("table")->delete()`, the generated query will look like this:
```sql
DELETE FROM `table` WHERE
```

This is obviously invalid SQL syntax. This PR only adds the `WHERE` keyword and rules if conditions have been supplied to either `where()` or with `delete([])`

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/51
2025-07-29 09:46:46 +02:00
ddcd8a2961 fix: $offset not set when passing integer to $limit in limit() (#50)
This PR just sets the `OFFSET` value to `0` if no offset is provided for all calls to `limit()`. This fixes a bug where no `OFFSET` was set if the `$limit` parameter was given an integer.. which was the only allowed type except null anyways.

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/50
2025-07-17 11:09:02 +02:00
e062930c41 fix: add missing return statement from deprecated 'for()' method (#49)
Follow-up PR from #46. Forgot to return from the deprecated method.

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/49
2025-06-12 12:44:26 +02:00
vlw
814070a52e doc(fix): missed reference of "for()" to "from()" in README (#48)
Of course I missed to change one reference of `for()` to `from()`.

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/48
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2025-06-12 12:44:10 +02:00
03868ae784 fix: MySQL->for() deprecation notice since version 3.5.7 (#47)
Wrong version referenced in the deprecation notice added in #46.

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/47
2025-06-08 12:32:38 +02:00
00cb7b3297
refactor: rename for() to from() for consistency with MySQL syntax (#46) 2025-06-08 12:01:21 +02:00
vlw
86f8f2ee76 doc: add short list of notable features as well as some style changes (#45)
Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/45
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2025-06-08 11:50:38 +02:00
c64eb96049 fix: add proper Order Enum handling for MySQL->order() (#43)
I made a rushed merge with #41 and it doesn't work properly.. it throws an exception when passing an `Order` enum to the method, because we're only accepting strings. #42

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/43
2025-01-30 09:33:10 +00:00
e65c74797b feat: add ORDER BY statement Enum (#41)
Importable with:
```php
use vlw\MySQL\Order
```
To be used with the `MySQL->order()` method, for example:
```php
$db->for("table")->order(["column" => Order::ASC])->select("*");
```

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/41
2025-01-30 08:16:27 +00:00
64c7bae3cf feat: add protected array property for where statement columns (#40)
This PR adds a compliment for the `MySQL->filter_values` property but for filter columns which can be accessed from a protected scope with `MySQL->filter_columns`

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/40
2025-01-16 13:53:30 +00:00
d5f1efb9b9 feat: expose SQL class properties to protected scope (#39)
Makes sense to make these accessible when extending the MySQL class.

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/39
2024-12-20 10:59:02 +00:00
vlw
619f43b3bf fix(doc): remove reference to removed method flatten() from README (#38)
Closes #14

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/38
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2024-09-25 13:28:15 +00:00
vlw
1727247fa7 fix: remove where() method for database models (#37)
This PR removes the `where()` method which I don't think is particularly useful and also very untested since I don't use it personally at all. It's also probably better to do in-model checking for table columns **before** sending it off to this library when required anyways.

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/37
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2024-09-25 13:27:55 +00:00
vlw
a536a3bec4 chore: rename of this package to "php-mysql" (#36)
The name I gave this years ago "libmysqldriver" never sat right with me.. okay it might be considered a library for mysql but it's *definitely* not a driver.

Reviewed-on: https://codeberg.org/vlw/php-mysql/pulls/36
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2024-09-25 13:27:20 +00:00
adc2fda90a
feat: store last select columns in property (#40) 2024-04-29 08:17:12 +00:00
a19ed09a34
feat(cleanup): remove flatten() class method (#39) 2024-04-29 08:14:45 +00:00
a26db46aae
feat: define custom MySQL WHERE operators (#38)
* feat: add MySQL operators enum

* feat(doc): add custom operators to README
2024-04-16 14:31:38 +00:00
51d62e1763
fix: add quotes around table names (#37) 2024-04-12 16:30:37 +00:00
73b5d858ff
feat: reset definers when a new query starts with for() (#35)
* feat: reset definers on new query start

* feat(doc): add FOR reference to README
2024-02-26 12:51:52 +00:00
98ed26a375
feat: add executor method for DELETE statements (#34)
* feat: add executor for DELETE statements

* feat(doc): add DELETE executor ref to README
2024-02-14 11:01:03 +00:00
df00b63f35
feat: INSERT specified columns by passing assoc array to method (#33)
* feat: named columns for INSERT statements

* feat(doc): add assoc array ref to README
2024-02-14 09:15:24 +00:00
17fa248edb
feat: second int argument instead of assoc array for LIMIT and OFFSET (#32)
* feat: pass null to reset statements

* feat: limit offset as second argument

* fix(doc): change limit method in README
2024-01-12 13:24:04 +01:00
5fefc5d19f
feat: pass null to reset statements (#31) 2024-01-12 13:22:06 +01:00
111bd5c822
fix(doc): example nitpicks in README (#30) 2024-01-11 04:14:22 +01:00
4779b8b824
fix: return types for executor methods (#29)
* fix: fix return types for executor methods

* fix(doc): fix return types for executor methods in README
2024-01-08 14:30:51 +01:00
03235df47b
fix: convert boolean to tinyint (#28) 2024-01-08 14:30:21 +01:00
4ffa2ee24f
feat: return mysqli_result from executor methods (#27)
* feat: return mysqli_result from executor methods

* feat(doc): add mysqli_results ref. to README
2024-01-08 13:14:28 +01:00
6 changed files with 350 additions and 248 deletions

204
README.md
View file

@ -1,34 +1,36 @@
# php-libmysqldriver # php-mysql
This library provides abstraction methods for common operations on MySQL-like databases like `SELECT`, `UPDATE`, and `INSERT` using method chaining for the various MySQL features. This is a simple abstraction library for MySQL DML operations.
For example: For example:
```php ```php
$db->for(string $table) MySQL->from(string $table)
->with(array $model) ->where(?array ...$conditions)
->where(array $filters) ->order(?array $order_by)
->order(array $order_by) ->limit(?int $limit = null, ?int $offset = null)
->limit(1) ->select(string|array|null $columns = null): mysqli_result|bool;
->select(array $columns): array|bool;
``` ```
which would be equivalent to the following in MySQL: which would be equivalent to the following in MySQL:
```sql ```sql
SELECT $columns FROM $table WHERE $filter ORDER BY $order_by LIMIT $limit; SELECT `columns` FROM `table` WHERE `filter` ORDER BY `order_by` LIMIT `limit`;
``` ```
> [!IMPORTANT] - All methods can be chained in any order (even multiple times) after a [`from()`](#from) as long as a [`select()`](#select), [`insert()`](#insert), [`update()`](#update), or [`delete()`](#delete) is the last method.
> This library is built on top of the PHP [`MySQL Improved`](https://www.php.net/manual/en/book.mysqli.php) extension and requires PHP 8.0 or newer. - Chaining the same method more than once will override its previous value. Passing `null` to any method that accepts it will unset its value completely.
## Install from composer ## Install from composer
``` ```
composer require victorwesterlund/libmysqldriver composer require vlw/mysql
``` ```
```php ```php
use libmysqldriver/MySQL; use vlw\MySQL\MySQL;
``` ```
> [!IMPORTANT]
> This library requires the [`MySQL Improved`](https://www.php.net/manual/en/book.mysqli.php) extension and PHP 8.0 or newer.
# Example / Documentation # Example / Documentation
Available statements Available statements
@ -37,6 +39,7 @@ Statement|Method
`SELECT`|[`select()`](#select) `SELECT`|[`select()`](#select)
`UPDATE`|[`update()`](#update) `UPDATE`|[`update()`](#update)
`INSERT`|[`insert()`](#insert) `INSERT`|[`insert()`](#insert)
`DELETE`|[`delete()`](#delete)
`WHERE`|[`where()`](#where) `WHERE`|[`where()`](#where)
`ORDER BY`|[`order()`](#order-by) `ORDER BY`|[`order()`](#order-by)
`LIMIT`|[`limit()`](#limit) `LIMIT`|[`limit()`](#limit)
@ -52,31 +55,46 @@ id|beverage_type|beverage_name|beverage_size
3|tea|black|15 3|tea|black|15
```php ```php
use libmysqldriver\MySQL; use vlw\MySQL\MySQL;
// Pass through: https://www.php.net/manual/en/mysqli.construct.php // Pass through: https://www.php.net/manual/en/mysqli.construct.php
$db = new MySQL($host, $user, $pass, $db); $db = new MySQL($host, $user, $pass, $db);
``` ```
All executor methods [`select()`](#select), [`update()`](#update), and [`insert()`](#insert) will return a [`mysqli_result`](https://www.php.net/manual/en/class.mysqli-result.php) object or boolean.
# FROM
```php
MySQL->from(
string $table
): self;
```
All queries start by chaining the `from(string $table)` method. This will define which database table the current query should be executed on.
*Example:*
```php
MySQL->from("beverages")->select("beverage_type");
```
# SELECT # SELECT
Use `MySQL->select()` to retrieve columns from a database table. Chain `MySQL->select()` anywhere after a [`MySQL->from()`](#from) to retrieve columns from a database table.
Pass an associative array of strings, CSV string, or null to this method to filter columns. Pass an associative array of strings, CSV string, or null to this method to filter columns.
```php ```php
$db->select( MySQL->select(
array|string|null $columns string|array|null $columns
): array|bool; ): mysqli_result|bool;
// Returns array of arrays for each row, or bool if no columns were defined
``` ```
In most cases you probably want to select with a constraint. Chain the [`where()`](#where) method before `select()` to filter the query In most cases you probably want to select with a constraint. Chain the [`where()`](#where) method before `select()` to filter the query
### Example ### Example
```php ```php
$beverages = $db->for("beverages")->select(["beverage_name", "beverage_size"]); // SELECT beverage_name, beverage_size FROM beverages $`beverages` = MySQL->from("beverages")->select(["beverage_name", "beverage_size"]); // SELECT `beverage_name`, `beverage_size` FROM beverages
$beverages = $db->for("beverages")->select("beverage_name, beverage_size"); // SELECT beverage_name, beverage_size FROM beverages
``` ```
``` ```
[ [
@ -92,30 +110,14 @@ $beverages = $db->for("beverages")->select("beverage_name, beverage_size"); // S
] ]
``` ```
## Flatten array to single dimension
If you don't want an array of arrays and would instead like to access each key value pair directly. Chain the `MySQL->flatten()` anywhere before `MySQL->select()`.
This will return the key value pairs of the first entry directly.
> **Note**
> This method will not set `LIMIT 1` for you. It is recommended to chain `MySQL->limit(1)` anywhere before `MySQL->select()`. [You can read more about it here](https://github.com/VictorWesterlund/php-libmysqldriver/issues/14)
```php
$coffee = $db->for("beverages")->limit(1)->flatten()->select(["beverage_name", "beverage_size"]); // SELECT beverage_name, beverage_size FROM beverages WHERE beverage_type = "coffee" LIMIT 1
```
```php
[
"beverage_name" => "cappuccino",
"beverage_size" => 10
]
```
# INSERT # INSERT
Use `MySQL->insert()` to append a new row to a database table Chain `MySQL->insert()` anywhere after a [`MySQL->from()`](#from) to append a new row to a database table.
Passing a sequential array to `insert()` will assume that you wish to insert data for all defined columns in the table. Pass an associative array of `[column_name => value]` to INSERT data for specific columns (assuming the other columns have a [DEFAULT](https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html) value defined).
```php ```php
$db->insert( MySQL->insert(
// Array of values to INSERT // Array of values to INSERT
array $values array $values
): bool ): bool
@ -125,13 +127,38 @@ $db->insert(
#### Example #### Example
```php ```php
$db->for("beverages")->insert([ MySQL->from("beverages")->insert([
null, null,
"coffee", "coffee",
"latte", "latte",
10 10
]); ]);
// INSERT INTO beverages VALUES (null, "coffee", "latte", 10); // INSERT INTO `beverages` VALUES (null, "coffee", "latte", 10);
```
```
true
```
# DELETE
Chain `MySQL->delete()` anywhere after a [`MySQL->from()`](#from) to remove a row or rows from the a database table.
```php
MySQL->delete(
array ...$conditions
): bool
// Returns true if at least one row was deleted
```
This method takes at least one [`MySQL->where()`](#where)-syntaxed argument to determine which row or rows to delete. Refer to the [`MySQL->where()`](#where) section for more information.
#### Example
```php
MySQL->from("beverages")->delete([
"beverage_name" => "coffee",
]);
// DELETE FROM `beverages` WHERE `beverage_name` = "coffee";
``` ```
``` ```
true true
@ -139,34 +166,42 @@ true
# UPDATE # UPDATE
Modify existing rows with `MySQL->update()` Chain `MySQL->update()` anywhere after a [`MySQL->from()`](#from) to modify existing rows in a database table.
```php ```php
$db->get( MySQL->update(
// Key, value array of column names and values to update // Key, value array of column names and values to update
array $fields, array $fields,
): bool; ): mysqli_result|bool;
// Returns true if at least 1 row was changed // Returns true if at least 1 row was changed
``` ```
### Example ### Example
```php ```php
$db->for("beverages")->update(["beverage_size" => 10]); // UPDATE beverages SET beverage_size = 10 MySQL->from("beverages")->update(["beverage_size" => 10]); // UPDATE `beverages` SET `beverage_size` = 10
``` ```
```php ```php
true true
``` ```
In most cases you probably want to UPDATE against a constaint. Chain a [`where()`](#where) method before `update()` to set constraints In most cases you probably want to UPDATE against a constaint. Chain a [`where()`](#where) method before [`MySQL->update()`](#update) to set constraints
# WHERE # WHERE
Filter a `select()` or `update()` method by chaining the `MySQL->where()` method anywhere before it. Filter a [`MySQL->select()`](#select) or [`MySQL->update()`](#update) method by chaining the `MySQL->where()` method anywhere before it. The [`MySQL->delete()`](#delete) executor method also uses the same syntax for its arguments.
Each key, value pair will be `AND` constrained against each other.
```php
MySQL->where(
?array ...$conditions
): self;
```
### Example ### Example
```php ```php
$coffee = $db->for("beverages")->where(["beverage_type" => "coffee"])->select(["beverage_name", "beverage_size"]); // SELECT beverage_name, beverage_size FROM beverages WHERE (beverage_type = "coffee"); $coffee = MySQL->from("beverages")->where(["beverage_type" => "coffee"])->select(["beverage_name", "beverage_size"]); // SELECT `beverage_name`, `beverage_size` FROM `beverages` WHERE (`beverage_type` = "coffee");
``` ```
```php ```php
[ [
@ -181,9 +216,7 @@ $coffee = $db->for("beverages")->where(["beverage_type" => "coffee"])->select(["
] ]
``` ```
## Advanced filtering ## Capture groups
You can do more detailed filtering by passing more constraints into the same array, or even futher by passing multiple arrays each with filters.
### AND ### AND
@ -196,7 +229,7 @@ MySQL->where([
]); ]);
``` ```
```sql ```sql
WHERE (beverage_type = 'coffee' AND beverage_size = 15) WHERE (`beverage_type` = 'coffee' AND `beverage_size` = 15)
``` ```
### OR ### OR
@ -217,16 +250,41 @@ $filter2 = [
MySQL->where($filter1, $filter2, ...); MySQL->where($filter1, $filter2, ...);
``` ```
```sql ```sql
WHERE (beverage_type = 'coffee' AND beverage_size = 15) OR (beverage_type = 'tea' AND beverage_name = 'black') WHERE (`beverage_type` = 'coffee' AND `beverage_size` = 15) OR (`beverage_type` = 'tea' AND `beverage_name` = 'black')
``` ```
## Define custom operators
By default, all values in an the assoc array passed to `where()` will be treated as an `EQUALS` (=) operator.
```php
MySQL->where(["column" => "euqals_this_value"]);
```
Setting the value of any key to another assoc array will allow for more "advanced" filtering by defining your own [`Operators`](https://github.com/VictorWesterlund/php-libmysqldriver/blob/master/src/Operators.php).
The key of this subarray can be any MySQL operator string, or the **->value** of any case in the [`Operators`](https://github.com/VictorWesterlund/php-libmysqldriver/blob/master/src/Operators.php) enum.
```php
MySQL->where([
"beverage_name" => [
Operators::LIKE->value => "%wildcard_contains%"
]
]);
```
# ORDER BY # ORDER BY
Chain the `order()` method before a `select()` statement to order by a specific column Chain the `MySQL->order()` method before a [`MySQL->select()`](#select) statement to order by a specific column
```php ```php
$coffee = $db->for("beverages")->order(["beverage_name" => "ASC"])->select(["beverage_name", "beverage_size"]); // SELECT beverage_name, beverage_size FROM beverages ORDER BY beverage_name ASC MySQL->order(
?array $order_by
): self;
```
```php
$coffee = MySQL->from("beverages")->order(["beverage_name" => "ASC"])->select(["beverage_name", "beverage_size"]); // SELECT `beverage_name`, `beverage_size` FROM `beverages` ORDER BY `beverage_name` ASC
``` ```
```php ```php
[ [
@ -238,22 +296,26 @@ $coffee = $db->for("beverages")->order(["beverage_name" => "ASC"])->select(["bev
"beverage_name" => "tea", "beverage_name" => "tea",
"beverage_size" => 15 "beverage_size" => 15
], ],
// ...etc for "beverage_name = coffee" // ...etc for "`beverage_name` = coffee"
] ]
``` ```
# LIMIT # LIMIT
Chain the `limit()` method before a `select()` statement to limit the amount of columns returned Chain the `limit()` method before a [`MySQL->select()`](#select) statement to limit the amount of columns returned
> **Note** ```php
> You can also flatten to a single dimensional array from the first entity by chaining [`MySQL->flatten()`](#flatten-array-to-single-dimension) MySQL->limit(
?int $limit,
?int $offset = null
): self;
```
## Passing an integer to LIMIT ## Passing a single integer argument
This will simply `LIMIT` the results returned to the integer passed This will simply `LIMIT` the results returned to the integer passed
```php ```php
$coffee = $db->for("beverages")->limit(1)->select(["beverage_name", "beverage_size"]); // SELECT beverage_name, beverage_size FROM beverages WHERE beverage_type = "coffee" LIMIT 1 $coffee = MySQL->from("beverages")->limit(1)->select(["beverage_name", "beverage_size"]); // SELECT `beverage_name`, `beverage_size` FROM `beverages` WHERE `beverage_type` = "coffee" LIMIT 1
``` ```
```php ```php
[ [
@ -264,11 +326,11 @@ $coffee = $db->for("beverages")->limit(1)->select(["beverage_name", "beverage_si
] ]
``` ```
## Passing an associative array to LIMIT ## Passing two integer arguments
This will `OFFSET` and `LIMIT` the results returned from the first key of the array as `OFFSET` and the value of that key as `LIMIT` This will `OFFSET` and `LIMIT` the results returned. The first argument will be the `LIMIT` and the second argument will be its `OFFSET`.
```php ```php
$coffee = $db->for("beverages")->limit([3 => 2])->select(["beverage_name", "beverage_size"]); // SELECT beverage_name, beverage_size FROM beverages LIMIT 3 OFFSET 2 $coffee = MySQL->from("beverages")->limit(3, 2)->select(["beverage_name", "beverage_size"]); // SELECT `beverage_name`, `beverage_size` FROM `beverages` LIMIT 3 OFFSET 2
``` ```
```php ```php
[ [
@ -283,13 +345,3 @@ $coffee = $db->for("beverages")->limit([3 => 2])->select(["beverage_name", "beve
// ...etc // ...etc
] ]
``` ```
----
# Restrict affected/returned database columns to table model
Chain and pass an array to `MySQL->with()` before a `select()`, `update()`, or `insert()` method to limit which columns will be returned/affected. It will use the **values** of the array so it can be either sequential or associative.
**This method will cause `select()`, `update()`, and `insert()` to ignore any columns that are not present in the passed table model.**
You can remove an already set table model by passing `null` to `MySQL->with()`

View file

@ -1,18 +1,18 @@
{ {
"name": "victorwesterlund/libmysqldriver", "name": "vlw/mysql",
"description": "Abstraction library for common mysqli features", "description": "Abstraction library for common MySQL/MariaDB DML operations with php-mysqli",
"type": "library", "type": "library",
"license": "GPL-3.0-only", "license": "GPL-3.0-or-later",
"authors": [ "authors": [
{ {
"name": "Victor Westerlund", "name": "Victor Westerlund",
"email": "victor.vesterlund@gmail.com" "email": "victor@vlw.se"
} }
], ],
"minimum-stability": "dev", "minimum-stability": "dev",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"libmysqldriver\\": "src/" "vlw\\MySQL\\": "src/"
} }
}, },
"require": {} "require": {}

View file

@ -1,44 +0,0 @@
<?php
namespace libmysqldriver\Driver;
use \mysqli;
use \mysqli_stmt;
use \mysqli_result;
// MySQL query builder and executer abstractions
class DatabaseDriver extends mysqli {
// Passing arguments to https://www.php.net/manual/en/mysqli.construct.php
function __construct() {
parent::__construct(...func_get_args());
}
// Coerce input to single dimensional array
private static function to_list_array(mixed $input): array {
return array_values(is_array($input) ? $input : [$input]);
}
// Execute SQL query with optional prepared statement and return array of affected rows
public function exec(string $sql, mixed $params = null): array {
$query = $this->execute_query($sql, self::to_list_array($params));
$res = [];
// Fetch rows into sequential array
while ($row = $query->fetch_assoc()) {
$res[] = $row;
}
return $res;
}
// Execute SQL query with optional prepared statement and return true if query was successful
public function exec_bool(string $sql, mixed $params = null): bool {
$query = $this->execute_query($sql, self::to_list_array($params));
return gettype($query) === "boolean"
// Type is already a bool, so return it as is
? $query
// Return true if rows were matched
: $query->num_rows > 0;
}
}

View file

@ -1,78 +1,100 @@
<?php <?php
namespace libmysqldriver; namespace vlw\MySQL;
use \Exception; use Exception;
use \victorwesterlund\xEnum;
use mysqli;
use mysqli_stmt;
use mysqli_result;
use libmysqldriver\Driver\DatabaseDriver; use vlw\MySQL\Order;
use vlw\MySQL\Operators;
require_once "DatabaseDriver.php"; require_once "Order.php";
require_once "Operators.php";
// Interface for MySQL_Driver with abstractions for data manipulation // Interface for MySQL_Driver with abstractions for data manipulation
class MySQL extends DatabaseDriver { class MySQL extends mysqli {
private string $table; public ?array $columns = null;
private ?array $model = null;
protected string $table;
private bool $flatten = false; protected ?string $limit = null;
private ?string $order_by = null; protected ?string $order_by = null;
private ?string $filter_sql = null; protected array $filter_columns = [];
private array $filter_values = []; protected array $filter_values = [];
private int|string|null $limit = null; protected ?string $filter_sql = null;
// Pass constructor arguments to driver // Pass constructor arguments to driver
function __construct() { function __construct() {
parent::__construct(...func_get_args()); parent::__construct(...func_get_args());
} }
/*
# Helper methods
*/
private function throw_if_no_table() { private function throw_if_no_table() {
if (!$this->table) { if (!isset($this->table)) {
throw new Exception("No table name defined"); throw new Exception("No table name defined");
} }
} }
// Return value(s) that exist in $this->model // Coerce input to single dimensional array
private function in_model(string|array $columns): ?array { private static function to_list_array(mixed $input): array {
// Place string into array return array_values(is_array($input) ? $input : [$input]);
$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));
} }
/* ---- */ // 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 // Use the following table name
public function for(string $table): self { public function from(string $table): self {
// Reset all definers when a new query begins
$this->where();
$this->limit();
$this->order();
$this->table = $table; $this->table = $table;
return $this; return $this;
} }
// Restrict query to array of column names #[\Deprecated(
public function with(?array $model = null): self { message: "use MySQL->from() instead",
// Remove table model if empty since: "3.5.7",
if (!$model) { )]
$this->model = null; public function for(string $table): self {
return $this; return $this->from($table);
}
// 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;
} }
// Create a WHERE statement from filters // Create a WHERE statement from filters
public function where(array ...$conditions): self { 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 = []; $values = [];
$filters = []; $filters = [];
@ -86,21 +108,34 @@
} }
// Create SQL string and append values to array for prepared statement // Create SQL string and append values to array for prepared statement
foreach ($condition as $col => $value) { foreach ($condition as $col => $operation) {
if ($this->model && !$this->in_model($col)) { $this->filter_columns[] = $col;
continue;
// Assume we want an equals comparison if value is not an array
if (!is_array($operation)) {
$operation = [Operators::EQUALS->value => $operation];
} }
// Value is null so it does not need to be added to the prepared statement // Resolve all operator enum values in inner array
if (is_null($value)) { foreach ($operation as $operator => $value) {
$filter[] = "`{$col}` IS NULL"; // Null values have special syntax
continue; 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;
}
// Create SQL for prepared statement $filter[] = "`{$col}` IS NULL";
$filter[] = "`{$col}` = ?"; continue;
// Append value to array with all other values }
$values[] = $value;
// 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 // AND together all conditions into a group
@ -120,57 +155,53 @@
return $this; return $this;
} }
// Return SQL LIMIT string from integer or array of [offset => limit] // SQL LIMIT string
public function limit(int|array $limit): self { public function limit(?int $limit = null, ?int $offset = null): self {
// Set LIMIT without range directly as integer // Unset row limit if null was passed
if (is_int($limit)) { if ($limit === null) {
$this->limit = $limit; $this->limit = null;
return $this; return $this;
} }
// Use array key as LIMIT range start value // Coerce offset to zero if no offset is defined
$offset = (int) array_keys($limit)[0]; $offset = $offset ?? 0;
// Use array value as LIMIT range end value
$limit = (int) array_values($limit)[0];
// Set limit as SQL CSV // Set limit and offset as SQL CSV
$this->limit = "{$offset},{$limit}"; $this->limit = "{$offset},{$limit}";
return $this; 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 // Return SQL SORT BY string from assoc array of columns and direction
public function order(array $order_by): self { public function order(?array $order_by = null): self {
// Create CSV from columns // Unset row order by if null was passed
$sql = implode(",", array_keys($order_by)); if ($order_by === null) {
// Create pipe DSV from values $this->order_by = null;
$sql .= " " . implode("|", array_values($order_by)); return $this;
}
$this->order_by = $sql; // 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; return $this;
} }
/* ---- */ /*
# Executors
These methods execute various statements that each return a mysqli_result
*/
// Create Prepared Statament for SELECT with optional WHERE filters // Create Prepared Statament for SELECT with optional WHERE filters
public function select(array|string|null $columns = null): array|bool { public function select(array|string|null $columns = null): mysqli_result|bool {
$this->throw_if_no_table(); $this->throw_if_no_table();
// Create array of columns from CSV // Create array of columns from CSV
$columns = is_array($columns) || is_null($columns) ? $columns : explode(",", $columns); $this->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);
}
// Create CSV from columns or default to SQL NULL as a string // Create CSV from columns or default to SQL NULL as a string
$columns_sql = $columns ? implode(",", $columns) : "NULL"; $columns_sql = $this->columns ? implode(",", self::array_wrap_accents($this->columns)) : "NULL";
// Create LIMIT statement if argument is defined // Create LIMIT statement if argument is defined
$limit_sql = !is_null($this->limit) ? " LIMIT {$this->limit}" : ""; $limit_sql = !is_null($this->limit) ? " LIMIT {$this->limit}" : "";
@ -182,65 +213,74 @@
$filter_sql = !is_null($this->filter_sql) ? " WHERE {$this->filter_sql}" : ""; $filter_sql = !is_null($this->filter_sql) ? " WHERE {$this->filter_sql}" : "";
// Interpolate components into an SQL SELECT statmenet and execute // Interpolate components into an SQL SELECT statmenet and execute
$sql = "SELECT {$columns_sql} FROM {$this->table}{$filter_sql}{$order_by_sql}{$limit_sql}"; $sql = "SELECT {$columns_sql} FROM `{$this->table}`{$filter_sql}{$order_by_sql}{$limit_sql}";
// Return mysqli_response of matched rows
// No columns were specified, return true if query matched rows return $this->execute_query($sql, self::to_list_array($this->filter_values));
if (!$columns) {
return $this->exec_bool($sql, $this->filter_values);
}
// 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];
} }
// Create Prepared Statement for UPDATE using PRIMARY KEY as anchor // Create Prepared Statement for UPDATE using PRIMARY KEY as anchor
public function update(array $entity): bool { public function update(array $entity): mysqli_result|bool {
$this->throw_if_no_table(); $this->throw_if_no_table();
// 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");
}
}
}
// Create CSV string with Prepared Statement abbreviations from length of fields array. // Create CSV string with Prepared Statement abbreviations from length of fields array.
$changes = array_map(fn($column) => "{$column} = ?", array_keys($entity)); $changes = array_map(fn($column) => "`{$column}` = ?", array_keys($entity));
$changes = implode(",", $changes); $changes = implode(",", $changes);
// Get array of SQL WHERE string and filter values // Get array of SQL WHERE string and filter values
$filter_sql = !is_null($this->filter_sql) ? " WHERE {$this->filter_sql}" : ""; $filter_sql = !is_null($this->filter_sql) ? " WHERE {$this->filter_sql}" : "";
$values = array_values($entity); // Get values from entity and convert booleans to tinyint
// Append filter values if defined $values = self::filter_booleans(array_values($entity));
// Append values to filter property if where() was chained
if ($this->filter_values) { if ($this->filter_values) {
array_push($values, ...$this->filter_values); array_push($values, ...$this->filter_values);
} }
// Interpolate components into an SQL UPDATE statement and execute // Interpolate components into an SQL UPDATE statement and execute
$sql = "UPDATE {$this->table} SET {$changes} {$filter_sql}"; $sql = "UPDATE `{$this->table}` SET {$changes} {$filter_sql}";
return $this->exec_bool($sql, $values); return $this->execute_query($sql, self::to_list_array($values));
} }
// Create Prepared Statemt for INSERT // Create Prepared Statemt for INSERT
public function insert(array $values): bool { public function insert(array $values): mysqli_result|bool {
$this->throw_if_no_table(); $this->throw_if_no_table();
// A value for each column in table model must be provided /*
if ($this->model && count($values) !== count($this->model)) { Use array keys from $values as columns to insert if array is associative.
throw new Exception("Values length does not match columns in model"); 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. // Create CSV string with Prepared Statement abbreviatons from length of fields array.
$values_stmt = implode(",", array_fill(0, count($values), "?")); $values_stmt = implode(",", array_fill(0, count($values), "?"));
// Interpolate components into an SQL INSERT statement and execute // Interpolate components into an SQL INSERT statement and execute
$sql = "INSERT INTO {$this->table} VALUES ({$values_stmt})"; $sql = "INSERT INTO `{$this->table}` {$columns} VALUES ({$values_stmt})";
return $this->exec_bool($sql, $values); 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
if ($conditions) {
$this->where(...$conditions);
}
// Get array of SQL WHERE string and filter values
$filter_sql = !is_null($this->filter_sql) ? " WHERE {$this->filter_sql}" : "";
$sql = "DELETE FROM `{$this->table}`{$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));
} }
} }

46
src/Operators.php Normal file
View file

@ -0,0 +1,46 @@
<?php
namespace vlw\MySQL;
enum Operators: string {
// Logical
case ALL = "ALL";
case AND = "AND";
case ANY = "ANY";
case BETWEEN = "BETWEEN";
case EXISTS = "EXISTS";
case IN = "IN";
case LIKE = "LIKE";
case NOT = "NOT";
case OR = "OR";
case SOME = "SOME";
// Comparison
case EQUALS = "=";
case GT = ">";
case LT = "<";
case GTE = ">=";
case LTE = "<=";
case NOTE = "<>";
// Arithmetic
case ADD = "+";
case SUBTRACT = "-";
case MULTIPLY = "*";
case DIVIDE = "/";
case MODULO = "%";
// Bitwise
case BS_AND = "&";
case BS_OR = "|";
case BS_XOR = "^";
// Compound
case ADDE = "+=";
case SUBE = "-=";
case DIVE = "/=";
case MODE = "%=";
case BS_ANDE = "&=";
case BS_ORE = "|*=";
case BS_XORE = "^-=";
}

8
src/Order.php Normal file
View file

@ -0,0 +1,8 @@
<?php
namespace vlw\MySQL;
enum Order: string {
case ASC = "ASC";
case DESC = "DESC";
}