From 1e207c7806d9f83e707eb0281ebf6df47f804167 Mon Sep 17 00:00:00 2001 From: Wazabii Date: Mon, 4 Dec 2023 22:02:57 +0100 Subject: [PATCH 01/17] Minor changes --- AbstractDB.php | 4 +- DB.php | 2 +- Utility/Build.php | 309 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+), 3 deletions(-) create mode 100755 Utility/Build.php diff --git a/AbstractDB.php b/AbstractDB.php index cd7d2ad..9130a2c 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -219,7 +219,7 @@ final protected function setWhereData(string|AttrInterface $key, string|int|floa $this->whereProtocol[$key][] = $val; $this->resetWhere(); } - + /** * Get the Main FK data protocol * @return array @@ -411,4 +411,4 @@ final protected function query(string|self $sql, ?string $method = null, array $ } return $query; } -} +} \ No newline at end of file diff --git a/DB.php b/DB.php index c095a31..5b39eba 100755 --- a/DB.php +++ b/DB.php @@ -790,4 +790,4 @@ public function insertID(): int|string { return Connect::DB()->insert_id; } -} +} \ No newline at end of file diff --git a/Utility/Build.php b/Utility/Build.php new file mode 100755 index 0000000..8011922 --- /dev/null +++ b/Utility/Build.php @@ -0,0 +1,309 @@ +", ">=", "<", "<>", "!=", "<=", "<=>"]; // Comparison operators + protected const JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "CROSS"]; // Join types + protected const VIEW_PREFIX_NAME = "view"; // View prefix + + private $select; + + protected $table; + protected $join; + protected $joinedTables; + protected $limit; + protected $offset; + + protected $fkData; + protected $mig; + + protected $attr; + + + + public function __construct(object|array|null $obj = null) + { + + $this->attr = new \stdClass(); + if (!is_null($obj)) foreach($obj as $key => $value) { + $this->attr->{$key} = $value; + } + } + + + /** + * Will build where string + * @param string $prefix + * @param array $where + * @return string + */ + public function where(string $prefix, ?array $where): string + { + $out = ""; + if (!is_null($where)) { + $out = " {$prefix}"; + $index = 0; + foreach ($where as $array) { + $firstAnd = key($array); + $out .= (($index > 0) ? " {$firstAnd}" : "") . " ("; + $out .= $this->whereArrToStr($array); + $out .= ")"; + $index++; + } + } + return $out; + } + + + /** + * Build joins + * @return string + */ + public function join( + string|array|MigrateInterface $table, + string|array $where = null, + array $sprint = array(), + string $type = "INNER" + ): string + { + if ($table instanceof MigrateInterface) { + $this->join = array_merge($this->join, $this->buildJoinFromMig($table, $type)); + } else { + $this->buildJoinFromArgs($table, $where, $sprint, $type); + } + return (is_array($this->join)) ? " " . implode(" ", $this->join) : ""; + } + + /** + * Build limit + * @return string + */ + public function limit(): string + { + if (is_null($this->attr->limit) && !is_null($this->attr->offset)) { + $this->attr->limit = 1; + } + $offset = (!is_null($this->attr->offset)) ? ",{$this->attr->offset}" : ""; + return (!is_null($this->attr->limit)) ? " LIMIT {$this->attr->limit}{$offset}" : ""; + } + + + /** + * Build Where data + * @param array $array + * @return string + */ + final protected function whereArrToStr(array $array): string + { + $out = ""; + $count = 0; + foreach ($array as $key => $arr) { + foreach ($arr as $operator => $a) { + if (is_array($a)) { + foreach ($a as $col => $b) { + foreach ($b as $val) { + if ($count > 0) { + $out .= "{$key} "; + } + $out .= "{$col} {$operator} {$val} "; + $count++; + } + } + } else { + $out .= "{$key} {$a} "; + $count++; + } + } + } + + return $out; + } + + /** + * Build join data from Migrate data + * @param MigrateInterface $mig + * @param string $type Join type (INNER, LEFT, ...) + * @return array + */ + final protected function buildJoinFromMig(MigrateInterface $mig, string $type): array + { + $joinArr = array(); + $prefix = Connect::prefix(); + $main = $this->getMainFKData(); + $data = $mig->getData(); + $this->mig->mergeData($data); + $migTable = $mig->getTable(); + + foreach ($data as $col => $row) { + if (isset($row['fk'])) { + foreach ($row['fk'] as $a) { + if ($a['table'] === (string)$this->attr->table) { + $joinArr[] = "{$type} JOIN " . $prefix . $migTable . " " . $migTable . + " ON (" . $migTable . ".{$col} = {$a['table']}.{$a['column']})"; + } + } + } else { + foreach ($main as $c => $a) { + foreach ($a as $t => $d) { + if (in_array($col, $d)) { + $joinArr[] = "{$type} JOIN " . $prefix . $migTable . " " . $migTable . + " ON ({$t}.{$col} = {$this->attr->alias}.{$c})"; + } + } + } + } + + $this->joinedTables[$migTable] = $prefix . $migTable; + } + return $joinArr; + } + + + protected function buildJoinFromArgs( + string|array $table, + string|array $where, + array $sprint = array(), + string $type = "INNER" + ): void + { + if (is_null($where)) { + throw new \InvalidArgumentException("You need to specify the argumnet 2 (where) value!", 1); + } + + $prefix = Connect::prefix(); + $arr = $this->sperateAlias($table); + $table = (string)$this->prep($arr['table'], false); + $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " {$table}"; + + if (is_array($where)) { + $data = array(); + foreach ($where as $key => $val) { + if (is_array($val)) { + foreach ($val as $k => $v) { + $this->setWhereData($k, $v, $data); + } + } else { + $this->setWhereData($key, $val, $data); + } + } + $out = $this->buildWhere("", $data); + } else { + $out = $this->sprint($where, $sprint); + } + $type = $this->joinTypes(strtoupper($type)); // Whitelist + $this->join[] = "{$type} JOIN {$prefix}{$table}{$alias} ON " . $out; + $this->joinedTables[$table] = "{$prefix}{$table}"; + } + + /** + * Get the Main FK data protocol + * @return array + */ + final protected function getMainFKData(): array + { + if (is_null($this->fkData)) { + $this->fkData = array(); + foreach ($this->mig->getMig()->getData() as $col => $row) { + if (isset($row['fk'])) { + foreach ($row['fk'] as $a) { + $this->fkData[$col][$a['table']][] = $a['column']; + } + } + } + } + return $this->fkData; + } + + + /** + * Sperate Alias + * @param string|array $data + * @return array + */ + final protected function sperateAlias(string|array|DBInterface $data): array + { + $alias = null; + $table = $data; + if (is_array($data)) { + if (count($data) !== 2) { + throw new DBQueryException("If you specify Table as array then it should look " . + "like this [TABLE_NAME, ALIAS]", 1); + } + $alias = array_pop($data); + $table = reset($data); + } + return ["alias" => $alias, "table" => $table]; + } + + /** + * Mysql Prep/protect string + * @param mixed $val + * @return AttrInterface + */ + final protected function prep(mixed $val, bool $enclose = true): AttrInterface + { + if ($val instanceof AttrInterface) { + return $val; + } + $val = $this->getAttr($val); + $val->enclose($enclose); + return $val; + } + + /** + * Mysql Prep/protect array items + * @param array $arr + * @param bool $enclose + * @return array + */ + final protected function prepArr(array $arr, bool $enclose = true): array + { + $new = array(); + foreach ($arr as $pKey => $pVal) { + $key = (string)$this->prep($pKey, false); + $new[$key] = (string)$this->prep($pVal, $enclose); + } + return $new; + } + + /** + * Get new Attr instance + * @param array|string|int|float $value + * @return AttrInterface + */ + protected function getAttr(array|string|int|float $value): AttrInterface + { + return new Attr($value); + } + + /** + * Use vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually + * @param string $str SQL string example: (id = %d AND permalink = '%s') + * @param array $arr Mysql prep values + * @return string + */ + final protected function sprint(string $str, array $arr = array()): string + { + return vsprintf($str, $this->prepArr($arr, false)); + } + + /** + * Whitelist mysql join types + * @param string $val + * @return string + */ + protected function joinTypes(string $val): string + { + $val = trim($val); + if (in_array($val, $this::JOIN_TYPES)) { + return $val; + } + return "INNER"; + } +} From 55a280aa8ea89d74e3386750cd5708127d048bbf Mon Sep 17 00:00:00 2001 From: Wazabii Date: Tue, 5 Dec 2023 22:20:58 +0100 Subject: [PATCH 02/17] Transaction --- DB.php | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/DB.php b/DB.php index 5b39eba..b00a3af 100755 --- a/DB.php +++ b/DB.php @@ -763,6 +763,41 @@ public function sql(): string return $this->sql; } + /** + * Start Transaction + * @return mysqli + */ + public static function beginTransaction() + { + Connect::DB()->begin_transaction(); + return Connect::DB(); + } + + + // Same as @beginTransaction + public static function transaction() + { + return self::beginTransaction(); + } + + /** + * Commit transaction + * @return void + */ + public static function commit(): void + { + Connect::DB()->commit(); + } + + /** + * Rollback transaction + * @return void + */ + public static function rollback(): void + { + Connect::DB()->rollback(); + } + /** * Get return a new generated UUID * DEPRECATED: Will be moved to Connect for starter From fe9fa1e99550bc2c7c9710b396d612641c034e1b Mon Sep 17 00:00:00 2001 From: Wazabii Date: Thu, 7 Dec 2023 14:45:38 +0100 Subject: [PATCH 03/17] Data attr improvement --- Utility/Attr.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Utility/Attr.php b/Utility/Attr.php index d5a0aea..6b8021b 100755 --- a/Utility/Attr.php +++ b/Utility/Attr.php @@ -54,25 +54,29 @@ public function __toString(): string */ public function getValue(): string { - if (!$this->hasBeenEncoded) { $this->hasBeenEncoded = true; - if ($this->encode) { + // I will rather escape qoutes then encode them in prep is on $this->value = Encode::value($this->value)->encode(function ($val) { - if ($this->prep) { - $val = Connect::prep($val); - } return $val; }, ($this->prep ? ENT_NOQUOTES : ENT_QUOTES))->get(); + } else { - if ($this->prep) { + if (is_string($this->value)) { $this->value = Connect::prep($this->value); } } - if (is_array($this->value)) { - $this->value = json_encode($this->value); + if ($this->jsonEncode && is_array($this->value)) { + // If prep is on then escape after json_encode, + // otherwise json encode will possibly escape the escaped value + $this->value = json_encode($this->value, JSON_UNESCAPED_SLASHES); } + + if($this->prep) { + $this->value = Connect::prep($this->value); + } + if ($this->enclose) { $this->value = "'{$this->value}'"; } From 1a21e11c306ae92b9be950a4b7ecd1691e26b218 Mon Sep 17 00:00:00 2001 From: Wazabii Date: Fri, 8 Dec 2023 13:20:21 +0100 Subject: [PATCH 04/17] Improvements --- Utility/Attr.php | 39 ++++++++------------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/Utility/Attr.php b/Utility/Attr.php index 6b8021b..1b82913 100755 --- a/Utility/Attr.php +++ b/Utility/Attr.php @@ -55,24 +55,19 @@ public function __toString(): string public function getValue(): string { if (!$this->hasBeenEncoded) { + $this->hasBeenEncoded = true; - if ($this->encode) { - // I will rather escape qoutes then encode them in prep is on - $this->value = Encode::value($this->value)->encode(function ($val) { - return $val; - }, ($this->prep ? ENT_NOQUOTES : ENT_QUOTES))->get(); - - } else { - if (is_string($this->value)) { - $this->value = Connect::prep($this->value); - } - } + $this->value = Encode::value($this->value) + ->specialChar($this->encode, ($this->prep ? ENT_NOQUOTES : ENT_QUOTES)) + ->urlEncode(false) + ->encode(); + if ($this->jsonEncode && is_array($this->value)) { // If prep is on then escape after json_encode, // otherwise json encode will possibly escape the escaped value - $this->value = json_encode($this->value, JSON_UNESCAPED_SLASHES); + $this->value = json_encode($this->value); } - + if($this->prep) { $this->value = Connect::prep($this->value); } @@ -136,22 +131,4 @@ public function encode(bool $encode): self $this->encode = $encode; return $this; } - - /** - * // DEPRECATED - * If Request[key] is array then auto convert it to json to make it database ready - * @param bool $yes = true - * @return self - */ - /* - function mysqlVar(bool $mysqlVar = true): self - { - $this->mysqlVar = $mysqlVar; - $this->enclose = false; - $this->jsonEncode = false; - $this->encode = false; - $this->prep = false; - return $this; - } - */ } From fcffc39c997a9d195b7fa8763fad9ac049a63392 Mon Sep 17 00:00:00 2001 From: Wazabii Date: Tue, 12 Dec 2023 14:08:52 +0100 Subject: [PATCH 05/17] Added functionality --- AbstractDB.php | 73 ++++++++++++++++++++++++++++---------------------- DB.php | 10 +++++++ README.md | 64 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 111 insertions(+), 36 deletions(-) diff --git a/AbstractDB.php b/AbstractDB.php index 9130a2c..dce1e15 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -27,6 +27,7 @@ abstract class AbstractDB implements DBInterface protected $mig; protected $compare = "="; protected $whereAnd = "AND"; + protected $whereNot = false; protected $whereIndex = 0; protected $whereProtocol = []; protected $fkData; @@ -215,10 +216,49 @@ final protected function setWhereData(string|AttrInterface $key, string|int|floa throw new DBValidationException($this->mig->getMessage(), 1); } - $data[$this->whereIndex][$this->whereAnd][$this->compare][$key][] = $val; + //$data[$this->whereIndex][$this->whereAnd][$this->compare][$key][] = $val; + $data[$this->whereIndex][$this->whereAnd][$key][] = [ + "not" => $this->whereNot, + "operator" => $this->compare, + "value" => $val + ]; + $this->whereProtocol[$key][] = $val; $this->resetWhere(); } + + /** + * Build Where data + * @param array $array + * @return string + */ + final protected function whereArrToStr(array $array): string + { + $out = ""; + $count = 0; + foreach ($array as $key => $arr) { + foreach ($arr as $col => $a) { + if (is_array($a)) { + foreach ($a as $int => $row) { + if ($count > 0) { + $out .= "{$key} "; + } + if ($row['not'] === true) { + $out .= "NOT "; + } + $out .= "{$col} {$row['operator']} {$row['value']} "; + $count++; + } + + } else { + $out .= "{$key} {$a} "; + $count++; + } + } + } + + return $out; + } /** * Get the Main FK data protocol @@ -308,37 +348,6 @@ final protected function extractCamelCase(string $value): array return $arr; } - /** - * Build Where data - * @param array $array - * @return string - */ - final protected function whereArrToStr(array $array): string - { - $out = ""; - $count = 0; - foreach ($array as $key => $arr) { - foreach ($arr as $operator => $a) { - if (is_array($a)) { - foreach ($a as $col => $b) { - foreach ($b as $val) { - if ($count > 0) { - $out .= "{$key} "; - } - $out .= "{$col} {$operator} {$val} "; - $count++; - } - } - } else { - $out .= "{$key} {$a} "; - $count++; - } - } - } - - return $out; - } - /** * Build join data from Migrate data * @param MigrateInterface $mig diff --git a/DB.php b/DB.php index b00a3af..d3b5f7c 100755 --- a/DB.php +++ b/DB.php @@ -313,6 +313,16 @@ public function or(): self return $this; } + /** + * Chaining with where "NOT" + * @return self + */ + public function not(): self + { + $this->whereNot = true; + return $this; + } + /** * Raw Mysql Where input * Uses vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually diff --git a/README.md b/README.md index c5b257a..5dd8ac9 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ + # MaplePHP - MySQL queries MaplePHP - MySQL queries is a powerful yet **user-friendly** library for making **safe** database queries. -The guide is not complete; more content will be added soon. - +### Contents +- [Connect to the database](#connect-to-the-database) +- [Make queries](#make-queries) +- [Attributes](#attributes) +- *Migrations (Coming soon)* ## Connect to the database @@ -41,11 +45,14 @@ $select->where("id", 1); // id = '1' $select->where("parent", 0, ">"); // parent > '1' ``` ### Where 2 +"compare", "or"/"and" and "not". ```php $select->whereRoleStatusParent(1, 1, 0); // role = '1' AND status = '1' AND Parent = 0 $select->compare(">")->whereStatus(0)->or()->whereRole(1); // status > '0' OR role = '1' +$select->not()->whereId(1)->whereEmail("john.doe@gmail.com"); +// NOT id = '1' AND email = 'john.doe@gmail.com' ``` ### Where 3 ```php @@ -63,7 +70,7 @@ $select->whereRaw("status = %d AND visible = %d", [1, 1]); // PROTECTED: status = 1 AND visible = 1 ``` ### Having -Having command works the same as where command above with exception that you rename "where" in method to "having" and @havingBind do not exist. +Having command works the same as where command above with exception that you rename "where" method to "having" and that the method "havingBind" do not exist. ```php $select->having("id", 1); // id = '1' $select->having("parent", 0, ">"); // parent > '1' @@ -88,7 +95,7 @@ $select->order("id", "ASC")->order("parent", "DESC"); $select->orderRaw("id ASC, parent DESC"); // ORDER BY id ASC, parent DESC ``` -### Limit +### Join ```php $select->join("tableName", "b.user_id = a.id"); // Default INNER join $select->join("tableName", "b.user_id = '%d'", [872], "LEFT"); // PROTECTED INPUT @@ -134,3 +141,52 @@ $select->setRaw("msg_id", "UUID()"); ```php echo $select->sql(); ``` + +## Attributes +Each value is automatically escaped by default in the most effective manner to ensure consequential and secure data storage, guarding against SQL injection vulnerabilities. While it's possible to exert complete control over SQL input using various **Raw** methods, such an approach is not advisable due to the potential for mistakes that could introduce vulnerabilities. A safer alternative is to leverage the **Attr** class. The **Attr** class offers comprehensive configuration capabilities for nearly every value in the DB library, as illustrated below: +```php +$idValue = DB::withAttr("1") + ->prep(true) + ->enclose(true) + ->encode(true) + ->jsonEncode(true); + +$select->where("id", $idValue); +``` +#### Escape values and protect against SQL injections +```php +public function prep(bool $prep): self; +``` +**Example:** +- Input value: Lorem "ipsum" dolor +- Output value: Lorem \\"ipsum\\" dolor + +#### Enable/disable string enclose +```php +public function enclose(bool $enclose): self; +``` +**Example:** +- Input value: 1186 +- Output value: '1186' +*E.g. will add or remove quotes to values* + +#### Enable/disable XSS protection +Some like to have the all the database data already HTML special character escaped. +```php +public function encode(bool $encode): self; +``` +**Example:** +- Input value: Lorem ipsum dolor +- Output value: Lorem \ipsum\ dolor + +#### Automatically json encode array data +A pragmatic function that will automatically encode all array input data to a json string +```php +public function jsonEncode(bool $jsonEncode): self; +``` +**Example:** +- Input value: array("firstname" => "John", "lastname" => "Doe"); +- Output value: {"firstname":"John","lastname":"Doe"} + +The default values vary based on whether it is a table column, a condition in a WHERE clause, or a value to be set. For instance, columns default to enclose set to false, whereas for WHERE or SET inputs, it defaults to true. Regardless, every value defaults to **prep**, **encode** and **jsonEncode** being set to **true**. + From cdd7e3ef79f750b3455dace79830dd926248b923 Mon Sep 17 00:00:00 2001 From: Wazabii Date: Tue, 12 Dec 2023 14:34:56 +0100 Subject: [PATCH 06/17] Table prefix on/off --- Connect.php | 4 ++-- README.md | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Connect.php b/Connect.php index 121aa24..5b42126 100755 --- a/Connect.php +++ b/Connect.php @@ -17,7 +17,7 @@ class Connect private $charSetName; private $charset = "utf8mb4"; private static $self; - private static $prefix; + private static $prefix = ""; private static $selectedDB; private static $mysqlVars; @@ -54,7 +54,7 @@ public function setCharset(string $charset): void */ public static function setPrefix(string $prefix): void { - if (substr($prefix, -1) !== "_") { + if (strlen($prefix) > 0 && substr($prefix, -1) !== "_") { throw new \InvalidArgumentException("The Prefix has to end with a underscore e.g. (prefix\"_\")!", 1); } self::$prefix = $prefix; diff --git a/README.md b/README.md index 5dd8ac9..70330dc 100755 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ $connect->setPrefix("maple_"); $connect->execute(); ``` - ## Make queries Start with the namespace ```php @@ -30,10 +29,12 @@ use MaplePHP\Query\DB; ### Select 1: ```php -$select = DB::select("id,firstname,lastname", "users a")->whereId(1)->where("status", 0, ">")->limit(1); -$select->join("login b", "b.user_id = a.id"); +$select = DB::select("id,firstname,lastname", ["users", "aliasA"])->whereId(1)->where("status", 0, ">")->limit(1); +$select->join(["login", "aliasB"], "aliasB.user_id = aliasA.id"); $obj = $select->get(); // Get one row result as object ``` +*Default alias will be the table name e.g. users and login if they were not overwritten.* + ### Select 2: ```php $select = DB::select("id,name,content", "pages")->whereStatusParent(1, 0); @@ -97,12 +98,14 @@ $select->orderRaw("id ASC, parent DESC"); ``` ### Join ```php -$select->join("tableName", "b.user_id = a.id"); // Default INNER join +$select->join(["login", "aliasB"], ["aliasB.user_id" => "aliasA.id"]); // PROTECTED INPUT $select->join("tableName", "b.user_id = '%d'", [872], "LEFT"); // PROTECTED INPUT -$select->joinInner("tableName", "b.user_id = a.id"); -$select->joinLeft("tableName", "b.user_id = a.id"); -$select->joinRight("tableName", "b.user_id = a.id"); -$select->joinCross("tableName", "b.user_id = a.id"); +$select->join("tableName", "b.user_id = a.id"); // "UNPROTECTED" INPUT + +$select->joinInner("tableName", ["b.user_id" => "a.id"]); +$select->joinLeft("tableName", ["b.user_id" => "a.id"]); +$select->joinRight("tableName", ["b.user_id" => "a.id"]); +$select->joinCross("tableName", ["b.user_id" => "a.id"]); ``` ### Insert ```php From 06876ef9cac6e01c24e1087225cab2d98bc201e1 Mon Sep 17 00:00:00 2001 From: Wazabii Date: Wed, 13 Dec 2023 20:28:14 +0100 Subject: [PATCH 07/17] Patch --- DB.php | 16 ++++++++++------ README.md | 19 ++++++++++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/DB.php b/DB.php index d3b5f7c..c231aee 100755 --- a/DB.php +++ b/DB.php @@ -335,8 +335,8 @@ public function whereRaw(string $sql, ...$arr): self if (is_array($arr[0] ?? null)) { $arr = $arr[0]; } - $this->resetWhere(); $this->where[$this->whereIndex][$this->whereAnd][] = $this->sprint($sql, $arr); + $this->resetWhere(); return $this; } @@ -402,8 +402,8 @@ public function havingRaw(string $sql, ...$arr): self if (is_array($arr[0] ?? null)) { $arr = $arr[0]; } - $this->resetWhere(); $this->having[$this->whereIndex][$this->whereAnd][] = $this->sprint($sql, $arr); + $this->resetWhere(); return $this; } @@ -495,7 +495,6 @@ public function join( array $sprint = array(), string $type = "INNER" ): self { - if ($table instanceof MigrateInterface) { $this->join = array_merge($this->join, $this->buildJoinFromMig($table, $type)); } else { @@ -512,10 +511,16 @@ public function join( $data = array(); foreach ($where as $key => $val) { if (is_array($val)) { - foreach ($val as $k => $v) { - $this->setWhereData($k, $v, $data); + foreach ($val as $grpKey => $grpVal) { + if(!($grpVal instanceof AttrInterface)) { + $grpVal = $this::withAttr($grpVal)->enclose(false); + } + $this->setWhereData($grpKey, $grpVal, $data); } } else { + if(!($val instanceof AttrInterface)) { + $val = $this::withAttr($val)->enclose(false); + } $this->setWhereData($key, $val, $data); } } @@ -527,7 +532,6 @@ public function join( $this->join[] = "{$type} JOIN {$prefix}{$table}{$alias} ON " . $out; $this->joinedTables[$table] = "{$prefix}{$table}"; } - return $this; } diff --git a/README.md b/README.md index 70330dc..28bba7d 100755 --- a/README.md +++ b/README.md @@ -57,8 +57,9 @@ $select->not()->whereId(1)->whereEmail("john.doe@gmail.com"); ``` ### Where 3 ```php -$select->whereBind(function($inst) { - $select->where("start_date", "2023-01-01", ">=") +$select->whereBind(function($select) { + $select + ->where("start_date", "2023-01-01", ">=") ->where("end_date", "2023-01-14", "<="); })->or()->whereStatus(1); // (start_date >= '2023-01-01' AND end_date <= '2023-01-14') OR (status = '1') @@ -97,8 +98,20 @@ $select->orderRaw("id ASC, parent DESC"); // ORDER BY id ASC, parent DESC ``` ### Join +**Note** that no value in the join is, by default, enclosed. This means it will not add quotes to strings. This means it will attempt to add a database column by default if it is a string, and will return an error if the string column does not exist. If you want to enclose the value with quotes, use Attributes (see the section below). ```php $select->join(["login", "aliasB"], ["aliasB.user_id" => "aliasA.id"]); // PROTECTED INPUT + +$select->join("login", ["user_id" => "id"]); +// user_id = id AND org_id = oid + +// This will enclose and reset all protections +$slug = DB::withAttr("my-slug-value"); +$select->join("login", [ + ["slug" => $slug], + ["visible" => 1] +]); + $select->join("tableName", "b.user_id = '%d'", [872], "LEFT"); // PROTECTED INPUT $select->join("tableName", "b.user_id = a.id"); // "UNPROTECTED" INPUT @@ -191,5 +204,5 @@ public function jsonEncode(bool $jsonEncode): self; - Input value: array("firstname" => "John", "lastname" => "Doe"); - Output value: {"firstname":"John","lastname":"Doe"} -The default values vary based on whether it is a table column, a condition in a WHERE clause, or a value to be set. For instance, columns default to enclose set to false, whereas for WHERE or SET inputs, it defaults to true. Regardless, every value defaults to **prep**, **encode** and **jsonEncode** being set to **true**. +The default values vary based on whether it is a table column, a condition in a WHERE clause, or a value to be set. For instance, if expecting a table columns, the default is not to enclose value with quotes, whereas for WHERE or SET inputs, it defaults is to enclose the values. Regardless, every value defaults to **prep**, **encode** and **jsonEncode** being set to **true**. From 080835b24dfac7765fcee5fd3a3ec0cf6b0945a1 Mon Sep 17 00:00:00 2001 From: Wazabii Date: Sat, 9 Mar 2024 19:00:46 +0100 Subject: [PATCH 08/17] Add mysql port as arg --- Connect.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Connect.php b/Connect.php index 5b42126..aac06f9 100755 --- a/Connect.php +++ b/Connect.php @@ -16,17 +16,19 @@ class Connect private $dbname; private $charSetName; private $charset = "utf8mb4"; + private $port; private static $self; private static $prefix = ""; private static $selectedDB; private static $mysqlVars; - public function __construct($server, $user, $pass, $dbname) + public function __construct(string $server, string $user, string $pass, string $dbname, int $port = 3306) { $this->server = $server; $this->user = $user; $this->pass = $pass; $this->dbname = $dbname; + $this->port = $port; self::$self = $this; } @@ -66,7 +68,7 @@ public static function setPrefix(string $prefix): void */ public function execute(): void { - self::$selectedDB = new mysqli($this->server, $this->user, $this->pass, $this->dbname); + self::$selectedDB = new mysqli($this->server, $this->user, $this->pass, $this->dbname, $this->port); if (mysqli_connect_error()) { throw new ConnectException('Failed to connect to MySQL: ' . mysqli_connect_error(), 1); } From 3c40586845bcce2406832d8a799eca742cc00c4e Mon Sep 17 00:00:00 2001 From: Wazabii Date: Thu, 18 Apr 2024 09:44:51 +0200 Subject: [PATCH 09/17] Add raw column method --- AbstractDB.php | 3 +++ DB.php | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/AbstractDB.php b/AbstractDB.php index dce1e15..9067932 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -109,6 +109,9 @@ public function getTable(bool $withAlias = false): string */ public function getColumns(): array { + if(is_string($this->columns)) { + return explode(",", $this->columns); + } if (!is_null($this->mig) && !$this->mig->columns($this->columns)) { throw new DBValidationException($this->mig->getMessage(), 1); } diff --git a/DB.php b/DB.php index c231aee..a2667f2 100755 --- a/DB.php +++ b/DB.php @@ -275,12 +275,28 @@ protected function showView(): self return $this; } - public function columns(...$columns) + /** + * Select protected mysql columns + * @param string $columns + * @return self + */ + public function columns(...$columns): self { $this->columns = $this->prepArr($columns, false); return $this; } + /** + * Select unprotected mysql columns + * @param string $columns + * @return self + */ + public function columnsRaw(string $columns): self + { + $this->columns = $columns; + return $this; + } + /** * Change where compare operator from default "=". * Will change back to default after where method is triggered From 2142ef37b80b5874b7abd1b50d4fd2d169f135c4 Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Fri, 31 May 2024 22:16:45 +0200 Subject: [PATCH 10/17] Add handler support Add handler support with mysql and sqlite --- AbstractDB.php | 6 +- AbstractMigrate.php | 21 ++-- Connect.php | 201 ++++++++++---------------------- Create.php | 180 ++++++++++++++-------------- DB.php | 58 +++------ Handlers/MySQLHandler.php | 179 ++++++++++++++++++++++++++++ Handlers/SQLiteHandler.php | 163 ++++++++++++++++++++++++++ Interfaces/HandlerInterface.php | 91 +++++++++++++++ Query.php | 13 ++- README.md | 19 ++- Utility/Attr.php | 2 +- Utility/Build.php | 4 +- 12 files changed, 643 insertions(+), 294 deletions(-) create mode 100755 Handlers/MySQLHandler.php create mode 100755 Handlers/SQLiteHandler.php create mode 100755 Interfaces/HandlerInterface.php diff --git a/AbstractDB.php b/AbstractDB.php index 9067932..912e089 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -90,7 +90,7 @@ abstract protected function showView(): self; */ public function connect() { - return Connect::DB(); + return Connect::getInstance()->DB(); } /** @@ -100,7 +100,7 @@ public function connect() public function getTable(bool $withAlias = false): string { $alias = ($withAlias && !is_null($this->alias)) ? " {$this->alias}" : ""; - return Connect::prefix() . $this->table . $alias; + return Connect::getInstance()->getHandler()->getPrefix() . $this->table . $alias; } /** @@ -360,7 +360,7 @@ final protected function extractCamelCase(string $value): array final protected function buildJoinFromMig(MigrateInterface $mig, string $type): array { $joinArr = array(); - $prefix = Connect::prefix(); + $prefix = Connect::getInstance()->getHandler()->getPrefix(); $main = $this->getMainFKData(); $data = $mig->getData(); $this->mig->mergeData($data); diff --git a/AbstractMigrate.php b/AbstractMigrate.php index 7a9b9fb..ba698eb 100755 --- a/AbstractMigrate.php +++ b/AbstractMigrate.php @@ -3,22 +3,23 @@ namespace MaplePHP\Query; -use MaplePHP\Query\Create; +//use MaplePHP\Query\Create; use MaplePHP\Query\Interfaces\MigrateInterface; +use Exception; abstract class AbstractMigrate implements MigrateInterface { protected $mig; protected $table; - abstract protected function buildTable(); + abstract protected function migrateTable(); public function __construct(string $table, ?string $prefix = null) { if (is_null($prefix)) { $prefix = getenv("MYSQL_PREFIX"); if ($prefix === false) { - throw new \Exception("Table prefix is required!", 1); + throw new Exception("Table prefix is required!", 1); } } $this->mig = new Create($table, $prefix); @@ -31,7 +32,7 @@ public function __construct(string $table, ?string $prefix = null) */ public function getBuild(): Create { - $this->buildTable(); + $this->migrateTable(); return $this->mig; } @@ -60,7 +61,7 @@ public function drop(): array */ public function getData(): array { - $this->buildTable(); + $this->migrateTable(); return $this->mig->getColumns(); } @@ -71,10 +72,7 @@ public function getData(): array public function read(): string { $this->mig->auto(); - $this->buildTable(); - if (!$this->mig->getColumns()) { - throw new \Exception("There is nothing to read in migration.", 1); - } + $this->migrateTable(); return $this->mig->build(); } @@ -85,10 +83,7 @@ public function read(): string public function create(): array { $this->mig->auto(); - $this->buildTable(); - if (!$this->mig->getColumns()) { - throw new \Exception("There is nothing to commit in migration.", 1); - } + $this->migrateTable(); return $this->mig->execute(); } diff --git a/Connect.php b/Connect.php index aac06f9..a85cde5 100755 --- a/Connect.php +++ b/Connect.php @@ -10,107 +10,96 @@ class Connect { - private $server; - private $user; - private $pass; - private $dbname; - private $charSetName; - private $charset = "utf8mb4"; - private $port; - private static $self; - private static $prefix = ""; - private static $selectedDB; - private static $mysqlVars; - public function __construct(string $server, string $user, string $pass, string $dbname, int $port = 3306) + private $handler; + private static array $inst; + private $db; + + private function __construct($handler) { - $this->server = $server; - $this->user = $user; - $this->pass = $pass; - $this->dbname = $dbname; - $this->port = $port; - self::$self = $this; + $this->handler = $handler; } /** - * Get current instance - * @return self + * Prevent cloning the instance + * @return void */ - public static function inst(): self - { - return self::$self; + private function __clone() { } - /** - * Set MySqli charset - * @param string $charset - */ - public function setCharset(string $charset): void + public static function __callStatic(string $name, array $arguments) { - $this->charset = $charset; + $inst = new DB(); + return $inst::$name(...$arguments); } - /** - * Set table prefix - * @param string $prefix - */ - public static function setPrefix(string $prefix): void + public static function setHandler($handler, ?string $key = null): self { - if (strlen($prefix) > 0 && substr($prefix, -1) !== "_") { - throw new \InvalidArgumentException("The Prefix has to end with a underscore e.g. (prefix\"_\")!", 1); + $key = self::getKey($key); + if(!self::hasInstance($key)) { + self::$inst[$key] = new self($handler); } - self::$prefix = $prefix; + return self::$inst[$key]; } - /** - * Connect to database - * @return void - */ - public function execute(): void + public static function getInstance(?string $key = null): self { - self::$selectedDB = new mysqli($this->server, $this->user, $this->pass, $this->dbname, $this->port); - if (mysqli_connect_error()) { - throw new ConnectException('Failed to connect to MySQL: ' . mysqli_connect_error(), 1); + $key = self::getKey($key); + if(!self::hasInstance($key)) { + throw new ConnectException("Connect Error: No Connection Found"); } - if (!mysqli_set_charset(self::$selectedDB, $this->charset)) { - throw new ConnectException("Error loading character set " . $this->charset . ": " . mysqli_error(self::$selectedDB), 2); - } - $this->charSetName = mysqli_character_set_name(self::$selectedDB); + + return self::$inst[$key]; + } + + private static function hasInstance(?string $key = null): bool + { + $key = self::getKey($key); + return (isset(self::$inst[$key]) && (self::$inst[$key] instanceof self)); + } + + private static function getKey(?string $key = null): string + { + $key = (is_null($key)) ? "default" : $key; + return $key; + } + + function getHandler() { + return $this->handler; } /** - * Get current DB connection + * Get database type + * @return string */ - public static function DB(): mysqli + public function getType(): string { - return static::$selectedDB; + return $this->handler->getType(); } - /** - * Get selected database name + * Get current table prefix * @return string */ - public function getDBName(): string + public function getPrefix(): string { - return $this->dbname; + return $this->handler->getPrefix(); } /** - * Get current Character set - * @return string|null + * Connect to database + * @return void */ - public function getCharSetName(): ?string + public function execute(): void { - return $this->charSetName; + $this->db = $this->handler->execute(); } /** - * Get current table prefix - * @return string + * Get current DB connection */ - public static function getPrefix(): string + public function DB(): mixed { - return static::$prefix; + return $this->db; } /** @@ -118,9 +107,9 @@ public static function getPrefix(): string * @param string $sql * @return object|array|bool */ - public static function query(string $sql): object|array|bool + public function query(string $sql): object|array|bool { - return static::DB()->query($sql); + return $this->db->query($sql); } /** @@ -128,9 +117,9 @@ public static function query(string $sql): object|array|bool * @param string $value * @return string */ - public static function prep(string $value): string + public function prep(string $value): string { - return static::DB()->real_escape_string($value); + return $this->handler->prep($value); } /** @@ -139,13 +128,15 @@ public static function prep(string $value): string * @param string|null $prefix Expected table prefix (NOT database prefix) * @return void */ - public static function selectDB(string $databaseName, ?string $prefix = null): void + /* + public static function selectDB(string $databaseName, ?string $prefix = null): void { mysqli_select_db(static::$selectedDB, $databaseName); if (!is_null($prefix)) { static::setPrefix($prefix); } } + */ /** * Execute multiple quries at once (e.g. from a sql file) @@ -153,82 +144,18 @@ public static function selectDB(string $databaseName, ?string $prefix = null): v * @param object|null &$mysqli * @return array */ - public static function multiQuery(string $sql, object &$mysqli = null): array + public function multiQuery(string $sql, object &$mysqli = null): array { - $count = 0; - $err = array(); - $mysqli = self::$selectedDB; - if (mysqli_multi_query($mysqli, $sql)) { - do { - if ($result = mysqli_use_result($mysqli)) { - /* - while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) { - } - */ - } - - if (!mysqli_more_results($mysqli)) { - break; - } - if (!mysqli_next_result($mysqli) || mysqli_errno($mysqli)) { - $err[$count] = mysqli_error($mysqli); - break; - } - $count++; - } while (true); - if ($result) { - mysqli_free_result($result); - } - } else { - $err[$count] = mysqli_error($mysqli); - } - - //mysqli_close($mysqli); - return $err; + return $this->handler->multiQuery($sql, $mysqli); } /** * Start Transaction * @return mysqli */ - public static function beginTransaction() - { - Connect::DB()->begin_transaction(); - return Connect::DB(); - } - - - // Same as @beginTransaction - public static function transaction() - { - return self::beginTransaction(); - } - - /** - * Commit transaction - * @return void - */ - public static function commit(): void - { - Connect::DB()->commit(); - } - - /** - * Rollback transaction - * @return void - */ - public static function rollback(): void - { - Connect::DB()->rollback(); - } - - /** - * Get current table prefix - * @return string - */ - public static function prefix(): string + public function transaction(): mixed { - return static::getPrefix(); + return $this->handler->transaction(); } /** diff --git a/Create.php b/Create.php index 86155fd..d4c9dae 100755 --- a/Create.php +++ b/Create.php @@ -1,69 +1,69 @@ rename(["test1", "test2"]); - - // Will create new stuff and alter current stuff - $mig->auto(); - - // Only create table - //$mig->create(); - - // Only alter table - //$mig->alter(); - - // Only drop table - //$mig->drop(); - - // Add/alter columns - $result = $mig->column("id", [ - "type" => "int", - "length" => 11, - "attr" => "unsigned", - "index" => "primary", - "ai" => true - - ])->column("testKey", [ - // Drop: Will drop the column - "drop" => true, - "type" => "int", - "length" => 11, - "index" => "index", - "attr" => "unsigned", - "default" => "0" - - ])->column("name", [ - "type" => "varchar", - "length" => 200, - "collate" => true, - "default" => "" - - ])->column("loremname_1", [ - // Rename: IF old column name is "loremname_1" or "loremname_2" then it will be renamed to "permalink" - "rename" => ["loremname_2", "permalink"], - "type" => "varchar", - "index" => "index", - "length" => 200, - "collate" => true - - ]); - - // Will execute migration - $mig->execute(); - - // Get migration in SQL string (CAN be called before @execute); - echo "
";
-    print_r($mig->build());
-    echo "
"; -*/ +// USAGE: + +// Init class +// Arg1: Table name +$mig = new \query\create("test"); + +// Rename: IF table name is "test" or "test1" then it will be renamed to "test2". +// Has to be the first method to be called +//$mig->rename(["test1", "test2"]); + +// Will create new stuff and alter current stuff +$mig->auto(); + +// Only create table +//$mig->create(); + +// Only alter table +//$mig->alter(); + +// Only drop table +//$mig->drop(); + +// Add/alter columns +$result = $mig->column("id", [ +"type" => "int", +"length" => 11, +"attr" => "unsigned", +"index" => "primary", +"ai" => true + +])->column("testKey", [ +// Drop: Will drop the column +"drop" => true, +"type" => "int", +"length" => 11, +"index" => "index", +"attr" => "unsigned", +"default" => "0" + +])->column("name", [ +"type" => "varchar", +"length" => 200, +"collate" => true, +"default" => "" + +])->column("loremname_1", [ +// Rename: IF old column name is "loremname_1" or "loremname_2" then it will be renamed to "permalink" +"rename" => ["loremname_2", "permalink"], +"type" => "varchar", +"index" => "index", +"length" => 200, +"collate" => true + +]); + +// Will execute migration +$mig->execute(); + +// Get migration in SQL string (CAN be called before @execute); +echo "
";
+print_r($mig->build());
+echo "
"; + */ namespace MaplePHP\Query; @@ -122,18 +122,18 @@ class Create public const INDEXES = ["PRIMARY", "UNIQUE", "INDEX", "FULLTEXT", "SPATIAL"]; public const FULLTEXT_COLUMNS = ["CHAR", "VARCHAR", "TEXT", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT", "JSON"]; public const SPATIAL_COLUMNS = ["POINT", "LINESTRING", "POLYGON", "GEOMETRY", "MULTIPOINT", - "MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION"]; + "MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION"]; public function __construct(string $table, ?string $prefix = null) { if (!is_null($prefix)) { - $this->prefix = Connect::prep($prefix); + $this->prefix = Connect::getInstance()->prep($prefix); } $this->charset = "utf8"; $this->engine = "InnoDB"; $this->rowFormat = "DYNAMIC"; - $this->tableText = Connect::prep($table); + $this->tableText = Connect::getInstance()->prep($table); $this->table = "{$this->prefix}{$this->tableText}"; } @@ -221,12 +221,12 @@ public function rename(array $arr): self foreach ($arr as $k => $tb) { $tb = ($k !== 0 ? $this->prefix : null) . $tb; if ($this->tableExists($tb)) { - $currentTB = Connect::prep($tb); + $currentTB = Connect::getInstance()->prep($tb); break; } } - $newTB = Connect::prep($this->prefix . end($arr)); + $newTB = Connect::getInstance()->prep($this->prefix . end($arr)); if ($currentTB) { $this->table = $currentTB; @@ -367,7 +367,7 @@ private function processColumnData() { foreach ($this->columns as $col => $arr) { - $col = Connect::prep($col); + $col = Connect::getInstance()->prep($col); $this->args = array_merge($this::ARGS, $arr); $this->hasRename = null; @@ -376,7 +376,7 @@ private function processColumnData() if (!$this->columnExists($this->table, $col)) { foreach ($hasRename as $k) { if ($this->columnExists($this->table, $k)) { - $col = Connect::prep($k); + $col = Connect::getInstance()->prep($k); break; } } @@ -498,7 +498,7 @@ public function build() switch ($this->type) { case "create": $this->build .= "(" . implode(",\n ", $this->add) . ") ENGINE={$this->engine} DEFAULT " . - "CHARSET={$this->charset} ROW_FORMAT={$this->rowFormat};"; + "CHARSET={$this->charset} ROW_FORMAT={$this->rowFormat};"; break; case "alter": $this->build .= "" . implode(",\n ", $this->add) . ";"; @@ -540,7 +540,7 @@ public function build() public function execute() { $sql = $this->build(); - $error = Connect::multiQuery($sql, $mysqli); + $error = Connect::getInstance()->multiQuery($sql, $mysqli); return $error; } @@ -548,7 +548,7 @@ public function mysqlCleanArr(array $arr) { $new = array(); foreach ($arr as $a) { - $new[] = Connect::prep($a); + $new[] = Connect::getInstance()->prep($a); } return $new; } @@ -562,7 +562,7 @@ private function tbKeys(): array if (is_null($this->tbKeys)) { $this->tbKeysType = $this->tbKeys = array(); if ($this->tableExists($this->table)) { - $result = Connect::query("SHOW INDEXES FROM {$this->table}"); + $result = Connect::getInstance()->query("SHOW INDEXES FROM {$this->table}"); if (is_object($result) && $result->num_rows > 0) { while ($row = $result->fetch_object()) { $type = ($row->Index_type === "FULLTEXT" || @@ -667,7 +667,7 @@ public function collation(): ?string if ($this->args['collate'] === true) { $this->args['collate'] = $this::COLLATION; } - return "CHARACTER SET " . Connect::prep($this->charset) . " COLLATE " . Connect::prep($this->args['collate']) . ""; + return "CHARACTER SET " . Connect::getInstance()->prep($this->charset) . " COLLATE " . Connect::getInstance()->prep($this->args['collate']) . ""; } return null; } @@ -688,7 +688,7 @@ public function null(): ?string public function default(): ?string { return (!is_null($this->args['default']) && $this->args['default'] !== false) ? "DEFAULT '" . - Connect::prep($this->args['default']) . "'" : null; + Connect::getInstance()->prep($this->args['default']) . "'" : null; } /** @@ -702,7 +702,7 @@ public function attributes(): ?string if (!in_array($this->args['attr'], $this::ATTRIBUTES)) { throw new QueryCreateException("The attribute \"{$this->args['attr']}\" does not exist", 1); } - return Connect::prep($this->args['attr']); + return Connect::getInstance()->prep($this->args['attr']); } return null; @@ -731,7 +731,7 @@ public function index(): ?string throw new QueryCreateException("You can ony have \"{$this->args['index']}\" index on column types " . "(" . implode(", ", static::FULLTEXT_COLUMNS) . "), you have \"{$this->args['type']}\".", 1); } - return Connect::prep($this->args['index']); + return Connect::getInstance()->prep($this->args['index']); } } return null; @@ -818,8 +818,8 @@ private function buildKeys(): string if (count($this->keys) > 0) { $sqlKeyArr = array(); foreach ($this->keys as $col => $key) { - $col = Connect::prep($col); - $key = strtoupper(Connect::prep($key)); + $col = Connect::getInstance()->prep($col); + $key = strtoupper(Connect::getInstance()->prep($key)); // Prepare DROP if (isset($prepareDrop[$col]) && ($index = array_search($key, $prepareDrop[$col])) !== false) { @@ -910,10 +910,10 @@ private function buildRename() public function fkExists(string $table, string $col) { - $table = Connect::prep($table); - $col = Connect::prep($col); - $dbName = Connect::inst()->getDBName(); - $result = Connect::query("SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM " . + $table = Connect::getInstance()->prep($table); + $col = Connect::getInstance()->prep($col); + $dbName = Connect::getInstance()->getDBName(); + $result = Connect::getInstance()->query("SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM " . "INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = '{$dbName}' AND " . "TABLE_NAME = '{$table}' AND COLUMN_NAME = '{$col}'"); @@ -933,8 +933,8 @@ public function tableExists(string $table = null) if (is_null($table)) { $table = $this->table; } - $table = Connect::prep($table); - $result = Connect::query("SHOW TABLES LIKE '{$table}'"); + $table = Connect::getInstance()->prep($table); + $result = Connect::getInstance()->query("SHOW TABLES LIKE '{$table}'"); if (is_object($result) && $result->num_rows > 0) { $this->tableExists = $result; } @@ -945,13 +945,13 @@ public function tableExists(string $table = null) public function columnExists(string $table, string $col) { if ($this->tableExists($table)) { - $table = Connect::prep($table); - $col = Connect::prep($col); - $result = Connect::query("SHOW COLUMNS FROM {$table} LIKE '{$col}'"); + $table = Connect::getInstance()->prep($table); + $col = Connect::getInstance()->prep($col); + $result = Connect::getInstance()->query("SHOW COLUMNS FROM {$table} LIKE '{$col}'"); if (is_object($result) && $result->num_rows > 0) { return $result; } } return false; } -} +} \ No newline at end of file diff --git a/DB.php b/DB.php index a2667f2..53492ff 100755 --- a/DB.php +++ b/DB.php @@ -3,6 +3,7 @@ namespace MaplePHP\Query; +use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\MigrateInterface; use MaplePHP\Query\Interfaces\DBInterface; @@ -45,6 +46,7 @@ public static function __callStatic($method, $args) $table = array_pop($args); $inst = self::table($table); $inst->method = $method; + $prefix = Connect::getInstance()->getHandler()->getPrefix(); switch ($inst->method) { case 'select': case 'selectView': @@ -57,13 +59,13 @@ public static function __callStatic($method, $args) case 'createView': case 'replaceView': $encodeArg1 = $inst->getAttr($defaultArgs[0])->enclose(false); - $inst->viewName = Connect::prefix() . static::VIEW_PREFIX_NAME . "_" . $encodeArg1; + $inst->viewName = $prefix . static::VIEW_PREFIX_NAME . "_" . $encodeArg1; $inst->sql = $defaultArgs[1]; break; case 'dropView': case 'showView': $encodeArg1 = $inst->getAttr($defaultArgs[0])->enclose(false); - $inst->viewName = Connect::prefix() . static::VIEW_PREFIX_NAME . "_" . $encodeArg1; + $inst->viewName = $prefix . static::VIEW_PREFIX_NAME . "_" . $encodeArg1; break; default: $inst->dynamic = [[$inst, $inst->method], $args]; @@ -518,7 +520,7 @@ public function join( throw new DBQueryException("You need to specify the argumnet 2 (where) value!", 1); } - $prefix = Connect::prefix(); + $prefix = Connect::getInstance()->getHandler()->getPrefix(); $arr = $this->sperateAlias($table); $table = (string)$this->prep($arr['table'], false); $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " {$table}"; @@ -749,7 +751,7 @@ private function buildJoin(): string } /** - * Byuld limit + * Build limit * @return string */ private function buildLimit(): string @@ -762,7 +764,8 @@ private function buildLimit(): string } /** - * Used to call methoed that builds SQL queryies + * Used to call method that builds SQL queries + * @throws DBQueryException */ final protected function build(): void { @@ -784,8 +787,9 @@ final protected function build(): void } /** - * Genrate SQL string of current instance/query + * Generate SQL string of current instance/query * @return string + * @throws DBQueryException */ public function sql(): string { @@ -795,47 +799,22 @@ public function sql(): string /** * Start Transaction - * @return mysqli - */ - public static function beginTransaction() - { - Connect::DB()->begin_transaction(); - return Connect::DB(); - } - - - // Same as @beginTransaction - public static function transaction() - { - return self::beginTransaction(); - } - - /** - * Commit transaction - * @return void - */ - public static function commit(): void - { - Connect::DB()->commit(); - } - - /** - * Rollback transaction - * @return void + * @return mixed + * @throws ConnectException */ - public static function rollback(): void + public static function transaction(): mixed { - Connect::DB()->rollback(); + return Connect::getInstance()->getHandler()->transaction(); } /** * Get return a new generated UUID * DEPRECATED: Will be moved to Connect for starter - * @return null|string + * @throws ConnectException|DBQueryException */ public static function getUUID(): ?string { - $result = Connect::query("SELECT UUID()"); + $result = Connect::getInstance()->query("SELECT UUID()"); if (is_object($result)) { if ($result->num_rows > 0) { $row = $result->fetch_row(); @@ -843,16 +822,17 @@ public static function getUUID(): ?string } return null; } else { - throw new DBQueryException(Connect::DB()->error, 1); + throw new DBQueryException(Connect::getInstance()->DB()->error, 1); } } /** * Get insert AI ID from prev inserted result * @return int|string + * @throws ConnectException */ public function insertID(): int|string { - return Connect::DB()->insert_id; + return Connect::getInstance()->DB()->insert_id; } } \ No newline at end of file diff --git a/Handlers/MySQLHandler.php b/Handlers/MySQLHandler.php new file mode 100755 index 0000000..d9d0f0e --- /dev/null +++ b/Handlers/MySQLHandler.php @@ -0,0 +1,179 @@ +server = $server; + $this->user = $user; + $this->pass = $pass; + $this->dbname = $dbname; + $this->port = $port; + } + + /** + * Get database type + * @return string + */ + public function getType(): string + { + return "mysql"; + } + + /** + * Set MySqli charset + * @param string $charset + */ + public function setCharset(string $charset): void + { + $this->charset = $charset; + } + + /** + * Set table prefix + * @param string $prefix + */ + public function setPrefix(string $prefix): void + { + if (strlen($prefix) > 0 && !str_ends_with($prefix, "_")) { + throw new InvalidArgumentException("The Prefix has to end with a underscore e.g. (prefix\"_\")!", 1); + } + $this->prefix = $prefix; + } + + /** + * Connect to database + * @return mysqli + * @throws ConnectException + */ + public function execute(): mysqli + { + $this->connection = new mysqli($this->server, $this->user, $this->pass, $this->dbname, $this->port); + if (mysqli_connect_error()) { + throw new ConnectException('Failed to connect to MySQL: ' . mysqli_connect_error(), 1); + } + if (!mysqli_set_charset($this->connection, $this->charset)) { + throw new ConnectException("Error loading character set " . $this->charset . ": " . mysqli_error($this->connection), 2); + } + $this->charSetName = mysqli_character_set_name($this->connection); + return $this->connection; + } + + /** + * Get selected database name + * @return string + */ + public function getDBName(): string + { + return $this->dbname; + } + + /** + * Get current Character set + * @return string|null + */ + public function getCharSetName(): ?string + { + return $this->charSetName; + } + + /** + * Get current table prefix + * @return string + */ + public function getPrefix(): string + { + return $this->prefix; + } + + /** + * Query sql string + * @param string $sql + * @return object|array|bool + */ + public function query(string $sql): object|array|bool + { + return $this->connection->query($sql); + } + + + /** + * Close MySQL Connection + * @return void + */ + public function close(): void + { + $this->connection->close(); + } + + + /** + * Protect/prep database values from injections + * @param string $value + * @return string + */ + public function prep(string $value): string + { + return $this->connection->real_escape_string($value); + } + + /** + * Execute multiple queries at once (e.g. from a sql file) + * @param string $sql + * @param object|null &$db + * @return array + */ + public function multiQuery(string $sql, object &$db = null): array + { + $count = 0; + $err = array(); + $db = $this->connection; + if (mysqli_multi_query($db, $sql)) { + do { + $result = mysqli_use_result($db); + if (!mysqli_more_results($db)) { + break; + } + if (!mysqli_next_result($db) || mysqli_errno($db)) { + $err[$count] = mysqli_error($db); + break; + } + $count++; + } while (true); + if ($result) { + mysqli_free_result($result); + } + } else { + $err[$count] = mysqli_error($db); + } + return $err; + } + + /** + * Start Transaction + * @return mysqli + */ + public function transaction(): mysqli + { + $this->connection->begin_transaction(); + return $this->connection; + } +} diff --git a/Handlers/SQLiteHandler.php b/Handlers/SQLiteHandler.php new file mode 100755 index 0000000..e80b5c5 --- /dev/null +++ b/Handlers/SQLiteHandler.php @@ -0,0 +1,163 @@ +database = $database; + } + + /** + * Get database type + * @return string + */ + public function getType(): string + { + return "sqlite"; + } + + /** + * Set SQLite charset + * @param string $charset + */ + public function setCharset(string $charset): void + { + $this->charset = $charset; + } + + /** + * Set table prefix + * @param string $prefix + */ + public function setPrefix(string $prefix): void + { + if (strlen($prefix) > 0 && !str_ends_with($prefix, "_")) { + throw new InvalidArgumentException("The Prefix has to end with an underscore e.g. (prefix\"_\")!", 1); + } + $this->prefix = $prefix; + } + + /** + * Connect to database + * @return SQLite3 + * @throws ConnectException + */ + public function execute(): SQLite3 + { + try { + $this->connection = new SQLite3($this->database); + + } catch (Exception $e) { + throw new ConnectException('Failed to connect to SQLite: ' . $e->getMessage(), 1); + } + return $this->connection; + } + + /** + * Get selected database name + * @return string + */ + public function getDBName(): string + { + return $this->database; + } + + /** + * Get current Character set + * @return string|null + */ + public function getCharSetName(): ?string + { + return $this->charSetName; + } + + /** + * Get current table prefix + * @return string + */ + public function getPrefix(): string + { + return $this->prefix; + } + + /** + * Query sql string + * @param string $sql + * @return SQLite3Result|false + */ + public function query(string $sql): bool|SQLite3Result + { + return $this->connection->query($sql); + } + + /** + * Close SQLite Connection + * @return void + */ + public function close(): void + { + $this->connection->close(); + } + + /** + * Protect/prep database values from injections + * @param string $value + * @return string + */ + public function prep(string $value): string + { + return SQLite3::escapeString($value); + } + + /** + * Execute multiple queries at once (e.g. from a sql file) + * @param string $sql + * @param object|null &$db + * @return array + */ + public function multiQuery(string $sql, object &$db = null): array + { + $count = 0; + $err = array(); + $queries = explode(";", $sql); + $db = $this->connection; + foreach ($queries as $query) { + if (trim($query) !== "") { + try { + $db->exec($query); + } catch (Exception $e) { + $err[$count] = $e->getMessage(); + break; + } + $count++; + } + } + return $err; + } + + /** + * Start Transaction + * @return SQLite3 + */ + public function transaction(): SQLite3 + { + $this->connection->exec('BEGIN TRANSACTION'); + return $this->connection; + } +} diff --git a/Interfaces/HandlerInterface.php b/Interfaces/HandlerInterface.php new file mode 100755 index 0000000..80c184c --- /dev/null +++ b/Interfaces/HandlerInterface.php @@ -0,0 +1,91 @@ +sql)) { + if ($result = Connect::getInstance()->query($this->sql)) { return $result; } else { - throw new ConnectException(Connect::DB()->error, 1); + throw new ConnectException(Connect::getInstance()->DB()->error, 1); } } @@ -44,6 +44,7 @@ public function get(): bool|object|array /** * SAME AS @get(): Execute query result And fetch as obejct * @return bool|object (Mysql result) + * @throws ConnectException */ final public function obj(): bool|object { @@ -56,8 +57,9 @@ final public function obj(): bool|object /** * Execute SELECT and fetch as array with nested objects - * @param callable|null $callback callaback, make changes in query and if return then change key + * @param callable|null $callback callaback, make changes in query and if return then change key * @return array + * @throws ConnectException */ final public function fetch(?callable $callback = null): array { @@ -89,9 +91,10 @@ final public function fetch(?callable $callback = null): array /** * Get insert AI ID from prev inserted result * @return string|int + * @throws ConnectException */ public function insertID(): string|int { - return Connect::DB()->insert_id; + return Connect::getInstance()->DB()->insert_id; } } diff --git a/README.md b/README.md index 28bba7d..a1b9832 100755 --- a/README.md +++ b/README.md @@ -13,13 +13,24 @@ MaplePHP - MySQL queries is a powerful yet **user-friendly** library for making ```php use MaplePHP\Query\Connect; -$connect = new Connect($server, $user, $password, $databaseName); -$connect->setCharset("utf8mb4"); +$handler = new MySQLHandler( + $server, + $user, + $password, + $databaseName, + $port +); + +// Maple DB do also support SQLite. +//$handler = new SqliteHandler("database.sqlite"); + // Recommened: Set TABLE prefix. This will make your life easier // MaplePHP DB class will "automatically prepend" it to the table names. -$connect->setPrefix("maple_"); -$connect->execute(); +$handler->setPrefix("maple_"); +$handler->setCharset("utf8mb4"); +$connect = Connect::setHandler($handler); +$connect->execute(); ``` ## Make queries Start with the namespace diff --git a/Utility/Attr.php b/Utility/Attr.php index 1b82913..e43587b 100755 --- a/Utility/Attr.php +++ b/Utility/Attr.php @@ -69,7 +69,7 @@ public function getValue(): string } if($this->prep) { - $this->value = Connect::prep($this->value); + $this->value = Connect::getInstance()->prep($this->value); } if ($this->enclose) { diff --git a/Utility/Build.php b/Utility/Build.php index 8011922..6a39a1a 100755 --- a/Utility/Build.php +++ b/Utility/Build.php @@ -134,7 +134,7 @@ final protected function whereArrToStr(array $array): string final protected function buildJoinFromMig(MigrateInterface $mig, string $type): array { $joinArr = array(); - $prefix = Connect::prefix(); + $prefix = Connect::getInstance()->getHandler()->getPrefix(); $main = $this->getMainFKData(); $data = $mig->getData(); $this->mig->mergeData($data); @@ -176,7 +176,7 @@ protected function buildJoinFromArgs( throw new \InvalidArgumentException("You need to specify the argumnet 2 (where) value!", 1); } - $prefix = Connect::prefix(); + $prefix = Connect::getInstance()->getHandler()->getPrefix(); $arr = $this->sperateAlias($table); $table = (string)$this->prep($arr['table'], false); $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " {$table}"; From b55b1e7c1e850e6e570a82edc90e7936ca381657 Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Thu, 6 Jun 2024 14:59:58 +0200 Subject: [PATCH 11/17] Add handler support --- AbstractDB.php | 35 +++--- Connect.php | 49 +++++++- Create.php | 2 +- DB.php | 169 ++++++++++++++++----------- Exceptions/ConnectException.php | 5 + Exceptions/DBQueryException.php | 5 + Exceptions/DBValidationException.php | 5 + Exceptions/QueryCreateException.php | 5 + Handlers/MySQLHandler.php | 12 +- Handlers/SQLiteHandler.php | 15 ++- Interfaces/HandlerInterface.php | 6 + Query.php | 24 +++- README.md | 67 ++++++++++- Utility/Attr.php | 2 +- Utility/WhitelistMigration.php | 5 +- tests/database.sqlite | Bin 0 -> 8192 bytes tests/unitary-db.php | 94 +++++++++++++++ 17 files changed, 397 insertions(+), 103 deletions(-) create mode 100644 tests/database.sqlite create mode 100755 tests/unitary-db.php diff --git a/AbstractDB.php b/AbstractDB.php index 912e089..c1dbfad 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -3,8 +3,8 @@ namespace MaplePHP\Query; +use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Utility\Attr; -use MaplePHP\Query\Handlers\MySqliHandler; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\MigrateInterface; use MaplePHP\Query\Interfaces\DBInterface; @@ -284,7 +284,8 @@ final protected function getMainFKData(): array /** * Mysql Prep/protect string - * @param mixed $val + * @param mixed $val + * @param bool $enclose * @return AttrInterface */ final protected function prep(mixed $val, bool $enclose = true): AttrInterface @@ -347,15 +348,15 @@ final protected function camelLoop(array $camelCaseArr, array $valArr, callable */ final protected function extractCamelCase(string $value): array { - $arr = preg_split('#([A-Z][^A-Z]*)#', $value, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - return $arr; + return preg_split('#([A-Z][^A-Z]*)#', $value, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); } /** * Build join data from Migrate data - * @param MigrateInterface $mig - * @param string $type Join type (INNER, LEFT, ...) + * @param MigrateInterface $mig + * @param string $type Join type (INNER, LEFT, ...) * @return array + * @throws ConnectException */ final protected function buildJoinFromMig(MigrateInterface $mig, string $type): array { @@ -370,16 +371,16 @@ final protected function buildJoinFromMig(MigrateInterface $mig, string $type): if (isset($row['fk'])) { foreach ($row['fk'] as $a) { if ($a['table'] === (string)$this->table) { - $joinArr[] = "{$type} JOIN " . $prefix . $migTable . " " . $migTable . - " ON (" . $migTable . ".{$col} = {$a['table']}.{$a['column']})"; + $joinArr[] = "$type JOIN " . $prefix . $migTable . " " . $migTable . + " ON (" . $migTable . ".$col = {$a['table']}.{$a['column']})"; } } } else { foreach ($main as $c => $a) { foreach ($a as $t => $d) { if (in_array($col, $d)) { - $joinArr[] = "{$type} JOIN " . $prefix . $migTable . " " . $migTable . - " ON ({$t}.{$col} = {$this->alias}.{$c})"; + $joinArr[] = "$type JOIN " . $prefix . $migTable . " " . $migTable . + " ON ($t.$col = $this->alias.$c)"; } } } @@ -407,19 +408,21 @@ protected function getAllQueryTables(): ?string /** * Query result - * @param string|self $sql - * @param string|null $method - * @param array $args - * @return array|object|bool + * @param string|self $sql + * @param string|null $method + * @param array $args + * @return array|object|bool|string + * @throws DBQueryException */ - final protected function query(string|self $sql, ?string $method = null, array $args = []): array|object|bool + final protected function query(string|self $sql, ?string $method = null, array $args = []): array|object|bool|string { $query = new Query($sql); + $query->setPluck($this->pluck); if (!is_null($method)) { if (method_exists($query, $method)) { return call_user_func_array([$query, $method], $args); } - throw new DBQueryException("Method \"{$method}\" does not exists!", 1); + throw new DBQueryException("Method \"$method\" does not exists!", 1); } return $query; } diff --git a/Connect.php b/Connect.php index a85cde5..5f0e285 100755 --- a/Connect.php +++ b/Connect.php @@ -5,12 +5,16 @@ use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\AttrInterface; +use MaplePHP\Query\Interfaces\MigrateInterface; use MaplePHP\Query\Utility\Attr; use mysqli; +/** + * @method static select(string $string, string|array|MigrateInterface $array) + * @method static table(string $string) + */ class Connect { - private $handler; private static array $inst; private $db; @@ -27,12 +31,24 @@ private function __construct($handler) private function __clone() { } + /** + * Access query builder intance + * @param string $name + * @param array $arguments + * @return mixed + */ public static function __callStatic(string $name, array $arguments) { $inst = new DB(); return $inst::$name(...$arguments); } + /** + * Set connection handler + * @param $handler + * @param string|null $key + * @return self + */ public static function setHandler($handler, ?string $key = null): self { $key = self::getKey($key); @@ -42,6 +58,12 @@ public static function setHandler($handler, ?string $key = null): self return self::$inst[$key]; } + /** + * Get default instance or secondary instances with key + * @param string|null $key + * @return self + * @throws ConnectException + */ public static function getInstance(?string $key = null): self { $key = self::getKey($key); @@ -52,18 +74,32 @@ public static function getInstance(?string $key = null): self return self::$inst[$key]; } - private static function hasInstance(?string $key = null): bool + /** + * Check if default instance or secondary instances exist for key + * @param string|null $key + * @return bool + */ + public static function hasInstance(?string $key = null): bool { $key = self::getKey($key); return (isset(self::$inst[$key]) && (self::$inst[$key] instanceof self)); } + /** + * Get the possible connection key + * @param string|null $key + * @return string + */ private static function getKey(?string $key = null): string { $key = (is_null($key)) ? "default" : $key; return $key; } + /** + * Access the connection handler + * @return mixed + */ function getHandler() { return $this->handler; } @@ -85,6 +121,15 @@ public function getPrefix(): string return $this->handler->getPrefix(); } + /** + * Check if a connections is open + * @return bool + */ + public function hasConnection(): bool + { + return $this->handler->hasConnection(); + } + /** * Connect to database * @return void diff --git a/Create.php b/Create.php index d4c9dae..b805496 100755 --- a/Create.php +++ b/Create.php @@ -912,7 +912,7 @@ public function fkExists(string $table, string $col) { $table = Connect::getInstance()->prep($table); $col = Connect::getInstance()->prep($col); - $dbName = Connect::getInstance()->getDBName(); + $dbName = Connect::getInstance()->getHandler()->getDBName(); $result = Connect::getInstance()->query("SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM " . "INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = '{$dbName}' AND " . "TABLE_NAME = '{$table}' AND COLUMN_NAME = '{$col}'"); diff --git a/DB.php b/DB.php index 53492ff..d55d519 100755 --- a/DB.php +++ b/DB.php @@ -9,9 +9,12 @@ use MaplePHP\Query\Interfaces\DBInterface; use MaplePHP\Query\Exceptions\DBValidationException; use MaplePHP\Query\Exceptions\DBQueryException; -use MaplePHP\Query\Utility\Attr; +//use MaplePHP\Query\Utility\Attr; use MaplePHP\Query\Utility\WhitelistMigration; +/** + * @method pluck(string $string) + */ class DB extends AbstractDB { private $method; @@ -32,14 +35,17 @@ class DB extends AbstractDB private $viewName; private $sql; private $dynamic; + protected ?string $pluck = null; /** - * It is a semi-dynamic method builder that expects certain types of objects to be setted - * @param string $method - * @param array $args + * It is a semi-dynamic method builder that expects certain types of objects to be set + * @param string $method + * @param array $args * @return self + * @throws ConnectException + * @throws DBQueryException */ - public static function __callStatic($method, $args) + public static function __callStatic(string $method, array $args) { if (count($args) > 0) { $defaultArgs = $args; @@ -79,19 +85,26 @@ public static function __callStatic($method, $args) /** * Used to make methods into dynamic shortcuts - * @param string $method - * @param array $args - * @return mixed + * @param string $method + * @param array $args + * @return array|bool|DB|object + * @throws DBQueryException + * @throws DBValidationException|ConnectException */ - public function __call($method, $args) + public function __call(string $method, array $args) { $camelCaseArr = $this->extractCamelCase($method); $shift = array_shift($camelCaseArr); switch ($shift) { case "pluck": // Columns?? - if (is_array($args[0] ?? null)) { - $args = $args[0]; + $args = ($args[0] ?? ""); + if (str_contains($args, ",")) { + throw new DBQueryException("Your only allowed to pluck one database column!"); } + + $pluck = explode(".", $args); + + $this->pluck = trim(end($pluck)); $this->columns($args); break; case "where": @@ -119,10 +132,11 @@ public function __call($method, $args) } /** - * You can build queries like Larvel If you want. I do not think they have good semantics tho. + * You can build queries like Laravel If you want. I do not think they have good semantics tho. * It is better to use (DB::select, DB::insert, DB::update, DB::delete) - * @param string|array $table Mysql table name (if array e.g. [TABLE_NAME, ALIAS]) - * @return self new intance + * @param string|array|MigrateInterface $data + * @return self new instance + * @throws DBQueryException */ public static function table(string|array|MigrateInterface $data): self { @@ -145,27 +159,35 @@ public static function table(string|array|MigrateInterface $data): self /** * Access Query Attr class - * @param array|string|int|float $value + * @param array|string|int|float $value + * @param array|null $args * @return AttrInterface + * @throws DBValidationException */ public static function withAttr(array|string|int|float $value, ?array $args = null): AttrInterface { $inst = new self(); $inst = $inst->getAttr($value); if (!is_null($args)) { - foreach ($args as $method => $args) { + foreach ($args as $method => $arg) { if (!method_exists($inst, $method)) { throw new DBValidationException("The Query Attr method \"" .htmlspecialchars($method, ENT_QUOTES). "\" does not exists!", 1); } - $inst = call_user_func_array([$inst, $method], (!is_array($args) ? [$args] : $args)); + $inst = call_user_func_array([$inst, $method], (!is_array($arg) ? [$arg] : $arg)); } } return $inst; } + static function _select($col, $table) { + return self::table($table)->columns($col); + } + /** * Build SELECT sql code (The method will be auto called in method build) + * @method static __callStatic * @return self + * @throws DBValidationException */ protected function select(): self { @@ -176,8 +198,8 @@ protected function select(): self $order = (!is_null($this->order)) ? " ORDER BY " . implode(",", $this->order) : ""; $limit = $this->buildLimit(); - $this->sql = "{$this->explain}SELECT {$this->noCache}{$this->calRows}{$this->distinct}{$columns} FROM " . - $this->getTable(true) . "{$join}{$where}{$this->group}{$having}{$order}{$limit}{$this->union}"; + $this->sql = "{$this->explain}SELECT $this->noCache$this->calRows$this->distinct$columns FROM " . + $this->getTable(true) . "$join$where$this->group$having$order$limit$this->union"; return $this; } @@ -185,6 +207,7 @@ protected function select(): self /** * Select view * @return self + * @throws DBValidationException */ protected function selectView(): self { @@ -212,8 +235,8 @@ protected function update(): self $where = $this->buildWhere("WHERE", $this->where); $limit = $this->buildLimit(); - $this->sql = "{$this->explain}UPDATE " . $this->getTable() . "{$join} SET " . - $this->buildUpdateSet() . "{$where}{$limit}"; + $this->sql = "{$this->explain}UPDATE " . $this->getTable() . "$join SET " . + $this->buildUpdateSet() . "$where$limit}"; return $this; } @@ -225,13 +248,13 @@ protected function delete(): self { $linkedTables = $this->getAllQueryTables(); if (!is_null($linkedTables)) { - $linkedTables = " {$linkedTables}"; + $linkedTables = " $linkedTables"; } $join = $this->buildJoin(); $where = $this->buildWhere("WHERE", $this->where); $limit = $this->buildLimit(); - $this->sql = "{$this->explain}DELETE{$linkedTables} FROM " . $this->getTable() . "{$join}{$where}{$limit}"; + $this->sql = "{$this->explain}DELETE$linkedTables FROM " . $this->getTable() . "$join$where$limit"; return $this; } @@ -242,7 +265,7 @@ protected function delete(): self protected function createView(): self { //$this->select(); - $this->sql = "CREATE VIEW " . $this->viewName . " AS {$this->sql}"; + $this->sql = "CREATE VIEW " . $this->viewName . " AS $this->sql"; return $this; } @@ -253,7 +276,7 @@ protected function createView(): self protected function replaceView(): self { //$this->select(); - $this->sql = "CREATE OR REPLACE VIEW " . $this->viewName . " AS {$this->sql}"; + $this->sql = "CREATE OR REPLACE VIEW " . $this->viewName . " AS $this->sql"; return $this; } @@ -343,7 +366,7 @@ public function not(): self /** * Raw Mysql Where input - * Uses vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually + * Uses sprint to mysql prep/protect input in string. Prep string values needs to be enclosed manually * @param string $sql SQL string example: (id = %d AND permalink = '%s') * @param array $arr Mysql prep values * @return self @@ -361,10 +384,11 @@ public function whereRaw(string $sql, ...$arr): self /** * Create protected MySQL WHERE input * Supports dynamic method name calls like: whereIdStatus(1, 0) - * @param string $key Mysql column - * @param string|int|float|AttrInterface $val Equals to value - * @param string|null $operator Change comparison operator from default "=". + * @param string|AttrInterface $key Mysql column + * @param string|int|float|AttrInterface $val Equals to value + * @param string|null $operator Change comparison operator from default "=". * @return self + * @throws DBValidationException */ public function where(string|AttrInterface $key, string|int|float|AttrInterface $val, ?string $operator = null): self { @@ -378,7 +402,7 @@ public function where(string|AttrInterface $key, string|int|float|AttrInterface /** * Group mysql WHERE inputs - * @param callable $call Evere method where placed inside callback will be grouped. + * @param callable $call Every method where placed inside callback will be grouped. * @return self */ public function whereBind(callable $call): self @@ -394,10 +418,11 @@ public function whereBind(callable $call): self /** * Create protected MySQL HAVING input - * @param string $key Mysql column - * @param string|int|float|AttrInterface $val Equals to value - * @param string|null $operator Change comparison operator from default "=". + * @param string|AttrInterface $key Mysql column + * @param string|int|float|AttrInterface $val Equals to value + * @param string|null $operator Change comparison operator from default "=". * @return self + * @throws DBValidationException */ public function having(string|AttrInterface $key, string|int|float|AttrInterface $val, ?string $operator = null): self { @@ -410,7 +435,7 @@ public function having(string|AttrInterface $key, string|int|float|AttrInterface /** * Raw Mysql HAVING input - * Uses vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually + * Uses sprint to mysql prep/protect input in string. Prep string values needs to be enclosed manually * @param string $sql SQL string example: (id = %d AND permalink = '%s') * @param array $arr Mysql prep values * @return self @@ -426,7 +451,7 @@ public function havingRaw(string $sql, ...$arr): self } /** - * Add a limit and maybee a offset + * Add a limit and maybe an offset * @param int $limit * @param int|null $offset * @return self @@ -441,7 +466,7 @@ public function limit(int $limit, ?int $offset = null): self } /** - * Add a offset (if limit is not set then it will automatically become "1"). + * Add an offset (if limit is not set then it will automatically become "1"). * @param int $offset * @return self */ @@ -453,9 +478,10 @@ public function offset(int $offset): self /** * Set Mysql ORDER - * @param string $col Mysql Column - * @param string $sort Mysql sort type. Only "ASC" OR "DESC" is allowed, anything else will become "ASC". + * @param string|AttrInterface $col Mysql Column + * @param string $sort Mysql sort type. Only "ASC" OR "DESC" is allowed, anything else will become "ASC". * @return self + * @throws DBValidationException */ public function order(string|AttrInterface $col, string $sort = "ASC"): self { @@ -465,13 +491,13 @@ public function order(string|AttrInterface $col, string $sort = "ASC"): self throw new DBValidationException($this->mig->getMessage(), 1); } $sort = $this->orderSort($sort); // Whitelist - $this->order[] = "{$col} {$sort}"; + $this->order[] = "$col $sort"; return $this; } /** * Raw Mysql ORDER input - * Uses vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually + * Uses sprint to mysql prep/protect input in string. Prep string values needs to be closed manually * @param string $sql SQL string example: (id ASC, parent DESC) * @param array $arr Mysql prep values * @return self @@ -487,8 +513,9 @@ public function orderRaw(string $sql, ...$arr): self /** * Add group - * @param array $columns + * @param array $columns * @return self + * @throws DBValidationException */ public function group(...$columns): self { @@ -501,11 +528,14 @@ public function group(...$columns): self /** * Mysql JOIN query (Default: INNER) - * @param string|array|MigrateInterface $table Mysql table name (if array e.g. [TABLE_NAME, ALIAS]) or MigrateInterface instance - * @param array|string $where Where data (as array or string e.g. string is raw) - * @param array $sprint Use sprint to prep data - * @param string $type Type of join + * @param string|array|MigrateInterface $table Mysql table name (if array e.g. [TABLE_NAME, ALIAS]) or MigrateInterface instance + * @param string|array|null $where Where data (as array or string e.g. string is raw) + * @param array $sprint Use sprint to prep data + * @param string $type Type of join * @return self + * @throws ConnectException + * @throws DBQueryException + * @throws DBValidationException */ public function join( string|array|MigrateInterface $table, @@ -517,13 +547,13 @@ public function join( $this->join = array_merge($this->join, $this->buildJoinFromMig($table, $type)); } else { if (is_null($where)) { - throw new DBQueryException("You need to specify the argumnet 2 (where) value!", 1); + throw new DBQueryException("You need to specify the argument 2 (where) value!", 1); } $prefix = Connect::getInstance()->getHandler()->getPrefix(); $arr = $this->sperateAlias($table); $table = (string)$this->prep($arr['table'], false); - $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " {$table}"; + $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " $table"; if (is_array($where)) { $data = array(); @@ -547,8 +577,8 @@ public function join( $out = $this->sprint($where, $sprint); } $type = $this->joinTypes(strtoupper($type)); // Whitelist - $this->join[] = "{$type} JOIN {$prefix}{$table}{$alias} ON " . $out; - $this->joinedTables[$table] = "{$prefix}{$table}"; + $this->join[] = "$type JOIN $prefix$table$alias ON " . $out; + $this->joinedTables[$table] = "$prefix$table"; } return $this; } @@ -564,7 +594,7 @@ public function distinct(): self } /** - * Exaplain the mysql query. Will tell you how you can make improvements + * Explain the mysql query. Will tell you how you can make improvements * @return self */ public function explain(): self @@ -595,14 +625,14 @@ public function calcRows(): self /** * Create INSERT or UPDATE set Mysql input to insert - * @param string|array|AttrInterface $key (string) "name" OR (array) ["id" => 1, "name" => "Lorem ipsum"] - * @param string|array|AttrInterface $value If key is string then value will pair with key "Lorem ipsum" + * @param string|array|AttrInterface $key (string) "name" OR (array) ["id" => 1, "name" => "Lorem ipsum"] + * @param string|array|AttrInterface|null $value If key is string then value will pair with key "Lorem ipsum" * @return self */ public function set(string|array|AttrInterface $key, string|array|AttrInterface $value = null): self { if (is_array($key)) { - $this->set = array_merge($this->set, $this->prepArr($key, true)); + $this->set = array_merge($this->set, $this->prepArr($key)); } else { $this->set[(string)$key] = $this->prep($value); } @@ -610,9 +640,9 @@ public function set(string|array|AttrInterface $key, string|array|AttrInterface } /** - * UPROTECTED: Create INSERT or UPDATE set Mysql input to insert + * UNPROTECTED: Create INSERT or UPDATE set Mysql input to insert * @param string $key Mysql column - * @param string $value Input/insert value (UPROTECTED and Will not enclose) + * @param string $value Input/insert value (UNPROTECTED and Will not enclose) */ public function setRaw(string $key, string $value): self { @@ -637,7 +667,7 @@ public function onDuplicateKey($key = null, ?string $value = null): self $this->dupSet = array(); if (!is_null($key)) { if (is_array($key)) { - $this->dupSet = $this->prepArr($key, true); + $this->dupSet = $this->prepArr($key); } else { $this->dupSet[$key] = $this->prep($value); } @@ -676,6 +706,7 @@ public function unionRaw(string $sql, bool $allowDuplicate = false): self /** * Build on insert set sql string part + * @param array|null $arr * @return string */ private function buildInsertSet(?array $arr = null): string @@ -686,11 +717,12 @@ private function buildInsertSet(?array $arr = null): string $columns = array_keys($arr); $columns = implode(",", $columns); $values = implode(",", $this->set); - return "({$columns}) VALUES ({$values})"; + return "($columns) VALUES ($values)"; } /** * Build on update set sql string part + * @param array|null $arr * @return string */ private function buildUpdateSet(?array $arr = null): string @@ -700,7 +732,7 @@ private function buildUpdateSet(?array $arr = null): string } $new = array(); foreach ($arr as $key => $val) { - $new[] = "{$key} = {$val}"; + $new[] = "$key = $val"; } return implode(",", $new); } @@ -720,19 +752,19 @@ private function buildDuplicate(): string /** * Will build where string - * @param string $prefix - * @param array $where + * @param string $prefix + * @param array|null $where * @return string */ private function buildWhere(string $prefix, ?array $where): string { $out = ""; if (!is_null($where)) { - $out = " {$prefix}"; + $out = " $prefix"; $index = 0; foreach ($where as $array) { $firstAnd = key($array); - $out .= (($index > 0) ? " {$firstAnd}" : "") . " ("; + $out .= (($index > 0) ? " $firstAnd" : "") . " ("; $out .= $this->whereArrToStr($array); $out .= ")"; $index++; @@ -759,8 +791,8 @@ private function buildLimit(): string if (is_null($this->limit) && !is_null($this->offset)) { $this->limit = 1; } - $offset = (!is_null($this->offset)) ? ",{$this->offset}" : ""; - return (!is_null($this->limit)) ? " LIMIT {$this->limit}{$offset}" : ""; + $offset = (!is_null($this->offset)) ? ",$this->offset" : ""; + return (!is_null($this->limit)) ? " LIMIT $this->limit$offset" : ""; } /** @@ -773,16 +805,11 @@ final protected function build(): void $inst = (!is_null($this->dynamic)) ? call_user_func_array($this->dynamic[0], $this->dynamic[1]) : $this->{$this->method}(); if (is_null($inst->sql)) { - throw new DBQueryException("The Method \"{$inst->method}\" expect to return a sql " . + throw new DBQueryException("The Method 1 \"$inst->method\" expect to return a sql " . "building method (like return @select() or @insert()).", 1); } } else { - if (is_null($this->sql)) { - $method = is_null($this->method) ? "NULL" : $this->method; - throw new DBQueryException("Method \"{$method}\" does not exists! You need to create a method that with " . - "same name as static, that will build the query you are after. " . - "Take a look att method @method->select.", 1); - } + $this->select(); } } diff --git a/Exceptions/ConnectException.php b/Exceptions/ConnectException.php index 4ef9e68..3ba4b35 100755 --- a/Exceptions/ConnectException.php +++ b/Exceptions/ConnectException.php @@ -4,6 +4,11 @@ use Exception; +/** + * Class ConnectException + * + * @package MaplePHP\Query\Exceptions + */ class ConnectException extends Exception { } diff --git a/Exceptions/DBQueryException.php b/Exceptions/DBQueryException.php index edceb2a..1ffc502 100755 --- a/Exceptions/DBQueryException.php +++ b/Exceptions/DBQueryException.php @@ -4,6 +4,11 @@ use Exception; +/** + * Class DBQueryException + * + * @package MaplePHP\Query\Exceptions + */ class DBQueryException extends Exception { } diff --git a/Exceptions/DBValidationException.php b/Exceptions/DBValidationException.php index 69f60ca..59f653d 100755 --- a/Exceptions/DBValidationException.php +++ b/Exceptions/DBValidationException.php @@ -4,6 +4,11 @@ use Exception; +/** + * Class DBValidationException + * + * @package MaplePHP\Query\Exceptions + */ class DBValidationException extends Exception { } diff --git a/Exceptions/QueryCreateException.php b/Exceptions/QueryCreateException.php index a753a09..752a605 100755 --- a/Exceptions/QueryCreateException.php +++ b/Exceptions/QueryCreateException.php @@ -2,6 +2,11 @@ namespace MaplePHP\Query\Exceptions; +/** + * Class QueryCreateException + * + * @package MaplePHP\Query\Exceptions + */ class QueryCreateException extends \InvalidArgumentException { } diff --git a/Handlers/MySQLHandler.php b/Handlers/MySQLHandler.php index d9d0f0e..ee25f28 100755 --- a/Handlers/MySQLHandler.php +++ b/Handlers/MySQLHandler.php @@ -18,7 +18,7 @@ class MySQLHandler implements HandlerInterface private string $charset = "utf8mb4"; private int $port; private string $prefix = ""; - private mysqli $connection; + private ?mysqli $connection; public function __construct(string $server, string $user, string $pass, string $dbname, int $port = 3306) { @@ -59,6 +59,15 @@ public function setPrefix(string $prefix): void $this->prefix = $prefix; } + /** + * Check if a connections is open + * @return bool + */ + public function hasConnection(): bool + { + return (!is_null($this->connection) && $this->connection->ping()); + } + /** * Connect to database * @return mysqli @@ -176,4 +185,5 @@ public function transaction(): mysqli $this->connection->begin_transaction(); return $this->connection; } + } diff --git a/Handlers/SQLiteHandler.php b/Handlers/SQLiteHandler.php index e80b5c5..1a35623 100755 --- a/Handlers/SQLiteHandler.php +++ b/Handlers/SQLiteHandler.php @@ -16,7 +16,7 @@ class SQLiteHandler implements HandlerInterface private ?string $charSetName = null; private string $charset = "UTF-8"; private string $prefix = ""; - private SQLite3 $connection; + private ?SQLite3 $connection; public function __construct(string $database) { @@ -53,6 +53,19 @@ public function setPrefix(string $prefix): void $this->prefix = $prefix; } + /** + * Check if a connections is open + * @return bool + */ + public function hasConnection(): bool + { + if (!is_null($this->connection)) { + $result = $this->connection->querySingle('PRAGMA quick_check'); + return $result === 'ok'; + } + return false; + } + /** * Connect to database * @return SQLite3 diff --git a/Interfaces/HandlerInterface.php b/Interfaces/HandlerInterface.php index 80c184c..5fee943 100755 --- a/Interfaces/HandlerInterface.php +++ b/Interfaces/HandlerInterface.php @@ -28,6 +28,12 @@ public function setCharset(string $charset): void; */ public function setPrefix(string $prefix): void; + /** + * Check if a connections is open + * @return bool + */ + public function hasConnection(): bool; + /** * Connect to database * @return mixed diff --git a/Query.php b/Query.php index 7dfe812..28da6ef 100755 --- a/Query.php +++ b/Query.php @@ -9,6 +9,7 @@ class Query { private $sql; + private ?string $pluck = null; public function __construct(string|DBInterface $sql) { @@ -18,6 +19,10 @@ public function __construct(string|DBInterface $sql) } } + public function setPluck(?string $pluck): void + { + $this->pluck = $pluck; + } /** * Execute query result * @return object|array|bool @@ -34,23 +39,27 @@ public function execute(): object|array|bool /** * Execute query result And fetch as obejct - * @return bool|object|array + * @return bool|object|string */ - public function get(): bool|object|array + public function get(): bool|object|string { return $this->obj(); } /** * SAME AS @get(): Execute query result And fetch as obejct - * @return bool|object (Mysql result) + * @return bool|object|string (Mysql result) * @throws ConnectException */ - final public function obj(): bool|object + final public function obj(): bool|object|string { $result = $this->execute(); if (is_object($result) && $result->num_rows > 0) { - return $result->fetch_object(); + $obj = $result->fetch_object(); + if(!is_null($this->pluck)) { + $obj = $obj->{$this->pluck}; + } + return $obj; } return false; } @@ -69,6 +78,11 @@ final public function fetch(?callable $callback = null): array $result = $this->execute(); if (is_object($result) && $result->num_rows > 0) { while ($row = $result->fetch_object()) { + + if(!is_null($this->pluck)) { + $row = $row->{$this->pluck}; + } + if ($callback) { $select = $callback($row, $key); } diff --git a/README.md b/README.md index a1b9832..1f84242 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# MaplePHP - MySQL queries -MaplePHP - MySQL queries is a powerful yet **user-friendly** library for making **safe** database queries. +# MaplePHP - Database query builder +MaplePHP - Database query builder is a powerful yet **user-friendly** library for making **safe** database queries, with support for both MySQL and SQLite. ### Contents - [Connect to the database](#connect-to-the-database) @@ -10,6 +10,7 @@ MaplePHP - MySQL queries is a powerful yet **user-friendly** library for making ## Connect to the database +### Connect to MySQL ```php use MaplePHP\Query\Connect; @@ -22,9 +23,9 @@ $handler = new MySQLHandler( ); // Maple DB do also support SQLite. -//$handler = new SqliteHandler("database.sqlite"); +//$handler = new SQLiteHandler("database.SQLite"); -// Recommened: Set TABLE prefix. This will make your life easier +// Recommend: Set TABLE prefix. This will make your life easier // MaplePHP DB class will "automatically prepend" it to the table names. $handler->setPrefix("maple_"); $handler->setCharset("utf8mb4"); @@ -32,6 +33,20 @@ $handler->setCharset("utf8mb4"); $connect = Connect::setHandler($handler); $connect->execute(); ``` + +### Connect to SQLite +```php +use MaplePHP\Query\Connect; + +$SQLiteHandler = new SQLiteHandler(__DIR__ . "/database.SQLite"); +$SQLiteHandler->setPrefix("mp_"); +$connect = Connect::setHandler($SQLiteHandler); +$connect->execute(); +``` + +***Note:** That the first connection will count as the main connection if not overwritten. You can also have multiple connection, click here for more information.* + + ## Make queries Start with the namespace ```php @@ -131,6 +146,13 @@ $select->joinLeft("tableName", ["b.user_id" => "a.id"]); $select->joinRight("tableName", ["b.user_id" => "a.id"]); $select->joinCross("tableName", ["b.user_id" => "a.id"]); ``` + +### Pluck +```php +$pluck->pluck("a.name")->get(); +``` + + ### Insert ```php $insert = DB::insert("pages")->set(["id" => 36, "name" => "About us", "slug" => "about-us"])->onDupKey(); @@ -217,3 +239,40 @@ public function jsonEncode(bool $jsonEncode): self; The default values vary based on whether it is a table column, a condition in a WHERE clause, or a value to be set. For instance, if expecting a table columns, the default is not to enclose value with quotes, whereas for WHERE or SET inputs, it defaults is to enclose the values. Regardless, every value defaults to **prep**, **encode** and **jsonEncode** being set to **true**. + +## Multiple connections +You can have multiple connection to different MySQL or SQLite databases or both. + +```php +use MaplePHP\Query\Connect; + +// DB: 1 +$handler = new MySQLHandler( + $server, + $user, + $password, + $databaseName, + $port +); +$handler->setPrefix("maple_"); +$handler->setCharset("utf8mb4"); +$connect = Connect::setHandler($handler); +$connect->execute(); + +// DB: 2 +$SQLiteHandler = new SQLiteHandler(__DIR__ . "/database.SQLite"); +$SQLiteHandler->setPrefix("mp_"); +$connect = Connect::setHandler($SQLiteHandler, "myConnKey"); +$connect->execute(); + +// The connections have been made! +// Let's do some queries + +// DB-1 Query: Access the default MySQL database +$select = DB::select("id,firstname,lastname", "users")->whereId(1)->limit(1); +$obj = $select->get(); + +// DB-2 Query: Access the SQLite database +$select = Connect::getInstance("myConnKey")::select("id,name,create_date", "tags")->whereId(1)->limit(1); +$obj = $select->get(); +``` diff --git a/Utility/Attr.php b/Utility/Attr.php index e43587b..3dbe363 100755 --- a/Utility/Attr.php +++ b/Utility/Attr.php @@ -14,7 +14,7 @@ class Attr implements AttrInterface private $prep = true; private $enclose = true; private $jsonEncode = true; - private $encode = true; + private $encode = false; /** * Initiate the instance diff --git a/Utility/WhitelistMigration.php b/Utility/WhitelistMigration.php index 871a4b6..a8ee6fe 100755 --- a/Utility/WhitelistMigration.php +++ b/Utility/WhitelistMigration.php @@ -78,11 +78,13 @@ public function getMessage(): ?string /** * Validate: Whitelist columns + * @param array $columns * @return bool */ public function columns(array $columns): bool { - foreach ($columns as $name) { + /* + foreach ($columns as $name) { if (($colPrefix = strpos($name, ".")) !== false) { $name = substr($name, $colPrefix + 1); } @@ -93,6 +95,7 @@ public function columns(array $columns): bool } } } + */ return true; } diff --git a/tests/database.sqlite b/tests/database.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..aa5c1210f459f9c5a08777026bb8253e9d72ab5f GIT binary patch literal 8192 zcmeI1-EQ1O6vw@xWaE6ay#iI0(9udLO~rz}-c7<*2tv9@h_sZF3Wb|$)}Cwz>=|c1 zmV`U^QFsA(10I1}u6PUX`H!95+*(}V+_};IyXIQ(zW!;;8{gRv@M&KyftEl^pe4`}XbH3g zS^_PBmOx9OCD0Ohtq4?;PWSEm_dBncWz(D&?- zs8D(;2yWE#Uu|b`EyeWAYd&`vsP>?i6z%n+Awf@EOM1B84dPc}5`L?UV0Rriwc7!LakM@DJ<5CclXGi0R6n`F{wN~U-YkLs`+ z)Q`6?Y$mKH=5q-tJ_8nXh9xXeqK*4;Ki>B-7!JpyVRi3KU-u_T_*kImQK<6rf@WHA zSr;p&sY8FWax(lttE|KMNWv#9#hVJ$u(hlyv1QD!m!YfmpjKWl zS9318WHlP3gh9e%`Kp=SC?-dz+{--0TE}UT+08xRtrbQe_Tv%7yW=<-4|l818>_)y zl_ZB}Sbr`vWspF?{k@PgA$(YtWiGNSE@;$|yq2^mZU&JI>K3b5Q~C`wVD-ov4SW40 b_GLXBK?DAd-l*aH&)A%IR_kp350m*XOMaKl literal 0 HcmV?d00001 diff --git a/tests/unitary-db.php b/tests/unitary-db.php new file mode 100755 index 0000000..320584c --- /dev/null +++ b/tests/unitary-db.php @@ -0,0 +1,94 @@ +hasConnection()) { + $unit = new Unit(); + + // Add a title to your tests (not required) + $unit->addTitle("Testing MaplePHP Query library!"); + $unit->add("MySql Query builder", function ($inst) { + + $db = Connect::getInstance(); + $select = $db::select("id,a.name,b.name AS cat", ["test", "a"])->whereParent(0)->where("status", 0, ">")->limit(6); + $select->join(["test_category", "b"], "tid = id"); + + // 3 queries + $obj = $select->get(); + $arr = $select->fetch(); + $pluck = DB::table("test")->pluck("name")->get(); + + $inst->add($obj, [ + "isObject" => [], + "missingColumn" => function () use ($obj) { + return (isset($obj->name) && isset($obj->cat)); + } + ], "Data is missing"); + + $inst->add($arr, [ + "isArray" => [], + "noRows" => function () use ($arr) { + return (count($arr) > 0); + } + ], "Fetch feed empty"); + + $inst->add($pluck, [ + "isString" => [], + "length" => [1] + ], "Pluck is expected to return string"); + + $select = $db::select("id,test.name,test_category.name AS cat", new Test)->whereParent(0)->where("status", 0, ">")->limit(6); + $select->join(new TestCategory); + $obj = $select->obj(); + + $inst->add($obj, [ + "isObject" => [], + "missingColumn" => function () use ($obj) { + return (isset($obj->name) && isset($obj->cat)); + } + ], "Data is missing"); + }); + + /** + * This will test multiple databases AND + * validate sqLite database + */ + $unit->add("sqLite Query builder", function ($inst) { + + $sqLiteHandler = new SQLiteHandler(__DIR__ . "/database.sqlite"); + $sqLiteHandler->setPrefix("mp_"); + $connect = Connect::setHandler($sqLiteHandler, "mp"); + $connect->execute(); + + // Access sqLite connection + $select = Connect::getInstance("mp")::select("id,name,content", "test")->whereStatus(1)->limit(3); + $result = $select->fetch(); + $inst->add($select->fetch(), [ + "isArray" => [], + "rows" => function () use ($result) { + return (count($result) === 3); + } + ], "Fetch should equal to 3"); + }); + + $unit->execute(); + } + +} catch (ConnectException|DBValidationException|DBQueryException $e) { + echo $e->getMessage(); +} + + + + + From 04a9dc4ca5b78b1f799b2b39cc296397f8538e1a Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Thu, 6 Jun 2024 15:01:26 +0200 Subject: [PATCH 12/17] Bump version to v2.0.0 --- composer.json | 59 ++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 26 deletions(-) mode change 100755 => 100644 composer.json diff --git a/composer.json b/composer.json old mode 100755 new mode 100644 index 58ece73..89ef6a7 --- a/composer.json +++ b/composer.json @@ -1,28 +1,35 @@ { - "name": "maplephp/query", - "type": "library", - "description": "MaplePHP - MySQL queries is a powerful yet user-friendly library for making safe database queries", - "keywords": ["database", "query", "fetch", "mysql", "mysqli"], - "homepage": "https://wazabii.se", - "license": "Apache-2.0", - "authors": [ - { - "name": "Daniel Ronkainen", - "email": "daniel.ronkainen@wazabii.se" - }, - { - "name": "MaplePHP", - "homepage": "https://wazabii.se" - } - ], - "require": { - "php": ">=8.0", - "maplephp/dto": "^1.0" + "name": "maplephp/query", + "type": "library", + "description": "MaplePHP - MySQL queries is a powerful yet user-friendly library for making safe database queries", + "keywords": [ + "database", + "query", + "fetch", + "mysql", + "mysqli" + ], + "homepage": "https://wazabii.se", + "license": "Apache-2.0", + "authors": [ + { + "name": "Daniel Ronkainen", + "email": "daniel.ronkainen@wazabii.se" }, - "autoload": { - "psr-4": { - "MaplePHP\\Query\\": "" - } - }, - "minimum-stability": "dev" -} \ No newline at end of file + { + "name": "MaplePHP", + "homepage": "https://wazabii.se" + } + ], + "require": { + "php": ">=8.0", + "maplephp/dto": "^1.0" + }, + "autoload": { + "psr-4": { + "MaplePHP\\Query\\": "" + } + }, + "minimum-stability": "dev", + "version": "v2.0.0" +} From d85cb54cb70bf52be1ae7339e28553e7072f77ef Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Thu, 6 Jun 2024 15:07:28 +0200 Subject: [PATCH 13/17] Update readme --- README.md | 3 ++- composer.json | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f84242..758160f 100755 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ MaplePHP - Database query builder is a powerful yet **user-friendly** library fo - [Connect to the database](#connect-to-the-database) - [Make queries](#make-queries) - [Attributes](#attributes) +- [Multiple Connections](#multiple-connections) - *Migrations (Coming soon)* ## Connect to the database @@ -44,7 +45,7 @@ $connect = Connect::setHandler($SQLiteHandler); $connect->execute(); ``` -***Note:** That the first connection will count as the main connection if not overwritten. You can also have multiple connection, click here for more information.* +***Note:** That the first connection will count as the main connection if not overwritten. You can also have multiple connection, [click here](#multiple-connections) for more information.* ## Make queries diff --git a/composer.json b/composer.json index 89ef6a7..1aeae91 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,9 @@ "php": ">=8.0", "maplephp/dto": "^1.0" }, + "require-dev": { + "maplephp/unitary": "^1.0" + }, "autoload": { "psr-4": { "MaplePHP\\Query\\": "" From dc091783bcbe198e32a1d4d1d711ed920a534b9a Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Sun, 18 Aug 2024 12:36:42 +0200 Subject: [PATCH 14/17] Add PostgreSQL handler Add new handler and interfaces --- AbstractDB.php | 18 +- Connect.php | 35 +++- DB.php | 91 +++++----- Handlers/PostgreSQL/PostgreSQLConnect.php | 106 ++++++++++++ Handlers/PostgreSQL/PostgreSQLResult.php | 106 ++++++++++++ Handlers/PostgreSQLHandler.php | 192 +++++++++++++++++++++ Handlers/SQLite/SQLiteConnect.php | 96 +++++++++++ Handlers/SQLite/SQLiteResult.php | 193 ++++++++++++++++++++++ Handlers/SQLiteHandler.php | 23 +-- Interfaces/ConnectInterface.php | 50 ++++++ Interfaces/HandlerInterface.php | 8 +- Interfaces/ResultInterface.php | 64 +++++++ Query.php | 23 ++- README.md | 22 ++- 14 files changed, 938 insertions(+), 89 deletions(-) create mode 100755 Handlers/PostgreSQL/PostgreSQLConnect.php create mode 100755 Handlers/PostgreSQL/PostgreSQLResult.php create mode 100755 Handlers/PostgreSQLHandler.php create mode 100644 Handlers/SQLite/SQLiteConnect.php create mode 100644 Handlers/SQLite/SQLiteResult.php create mode 100644 Interfaces/ConnectInterface.php create mode 100644 Interfaces/ResultInterface.php diff --git a/AbstractDB.php b/AbstractDB.php index c1dbfad..e25cfbe 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -33,6 +33,7 @@ abstract class AbstractDB implements DBInterface protected $fkData; protected $joinedTables; + protected string $connKey = "default"; /** * Build SELECT sql code (The method will be auto called in method build) @@ -84,13 +85,21 @@ abstract protected function dropView(): self; */ abstract protected function showView(): self; + public function setConnKey(?string $key) { + $this->connKey = is_null($key) ? "default" : $key; + } + + public function connInst() { + return Connect::getInstance($this->connKey); + } + /** * Access Mysql DB connection * @return \mysqli */ public function connect() { - return Connect::getInstance()->DB(); + return $this->connInst()->DB(); } /** @@ -100,7 +109,7 @@ public function connect() public function getTable(bool $withAlias = false): string { $alias = ($withAlias && !is_null($this->alias)) ? " {$this->alias}" : ""; - return Connect::getInstance()->getHandler()->getPrefix() . $this->table . $alias; + return $this->connInst()->getHandler()->getPrefix() . $this->table . $alias; } /** @@ -361,7 +370,7 @@ final protected function extractCamelCase(string $value): array final protected function buildJoinFromMig(MigrateInterface $mig, string $type): array { $joinArr = array(); - $prefix = Connect::getInstance()->getHandler()->getPrefix(); + $prefix = $this->connInst()->getHandler()->getPrefix(); $main = $this->getMainFKData(); $data = $mig->getData(); $this->mig->mergeData($data); @@ -391,7 +400,6 @@ final protected function buildJoinFromMig(MigrateInterface $mig, string $type): return $joinArr; } - /** * Build on YB to col sql string part * @return string|null @@ -416,7 +424,7 @@ protected function getAllQueryTables(): ?string */ final protected function query(string|self $sql, ?string $method = null, array $args = []): array|object|bool|string { - $query = new Query($sql); + $query = new Query($sql, $this->connInst()); $query->setPluck($this->pluck); if (!is_null($method)) { if (method_exists($query, $method)) { diff --git a/Connect.php b/Connect.php index 5f0e285..9eb0bef 100755 --- a/Connect.php +++ b/Connect.php @@ -3,6 +3,7 @@ namespace MaplePHP\Query; +use InvalidArgumentException; use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\MigrateInterface; @@ -10,13 +11,14 @@ use mysqli; /** - * @method static select(string $string, string|array|MigrateInterface $array) + * @method static select(string $columns, string|array|MigrateInterface $table) * @method static table(string $string) */ class Connect { private $handler; private static array $inst; + public static string $current = "default"; private $db; private function __construct($handler) @@ -32,7 +34,7 @@ private function __clone() { } /** - * Access query builder intance + * Access query builder instance * @param string $name * @param array $arguments * @return mixed @@ -40,6 +42,7 @@ private function __clone() { public static function __callStatic(string $name, array $arguments) { $inst = new DB(); + $inst->setConnKey(self::$current); return $inst::$name(...$arguments); } @@ -52,12 +55,32 @@ public static function __callStatic(string $name, array $arguments) public static function setHandler($handler, ?string $key = null): self { $key = self::getKey($key); - if(!self::hasInstance($key)) { - self::$inst[$key] = new self($handler); + if(self::hasInstance($key)) { + throw new InvalidArgumentException("A handler is already connected with key \"$key\"!"); } + self::$inst[$key] = new self($handler); return self::$inst[$key]; } + /** + * Remove a handler + * @param string $key + * @return void + */ + public static function removeHandler(string $key): void + { + if($key === "default") { + throw new InvalidArgumentException("You can not remove the default handler!"); + } + if(!self::hasInstance($key)) { + throw new InvalidArgumentException("The handler with with key \"$key\" does not exist!"); + } + unset(self::$inst[$key]); + if(self::$current === $key) { + self::$current = "default"; + } + } + /** * Get default instance or secondary instances with key * @param string|null $key @@ -68,9 +91,9 @@ public static function getInstance(?string $key = null): self { $key = self::getKey($key); if(!self::hasInstance($key)) { - throw new ConnectException("Connect Error: No Connection Found"); + throw new ConnectException("Connection Error: No active connection or connection instance found."); } - + self::$current = $key; return self::$inst[$key]; } diff --git a/DB.php b/DB.php index d55d519..97714ef 100755 --- a/DB.php +++ b/DB.php @@ -35,6 +35,7 @@ class DB extends AbstractDB private $viewName; private $sql; private $dynamic; + private ?string $returning = null; protected ?string $pluck = null; /** @@ -52,7 +53,9 @@ public static function __callStatic(string $method, array $args) $table = array_pop($args); $inst = self::table($table); $inst->method = $method; - $prefix = Connect::getInstance()->getHandler()->getPrefix(); + $inst->setConnKey(Connect::$current); + $prefix = Connect::getInstance(Connect::$current)->getHandler()->getPrefix(); + switch ($inst->method) { case 'select': case 'selectView': @@ -77,9 +80,11 @@ public static function __callStatic(string $method, array $args) $inst->dynamic = [[$inst, $inst->method], $args]; break; } + } else { $inst = new self(); } + return $inst; } @@ -151,6 +156,8 @@ public static function table(string|array|MigrateInterface $data): self $inst->alias = $data['alias']; $inst->table = $inst->getAttr($data['table'])->enclose(false); $inst->mig = $mig; + $inst->setConnKey(Connect::$current); + if (is_null($inst->alias)) { $inst->alias = $inst->table; } @@ -179,10 +186,6 @@ public static function withAttr(array|string|int|float $value, ?array $args = nu return $inst; } - static function _select($col, $table) { - return self::table($table)->columns($col); - } - /** * Build SELECT sql code (The method will be auto called in method build) * @method static __callStatic @@ -197,10 +200,8 @@ protected function select(): self $having = $this->buildWhere("HAVING", $this->having); $order = (!is_null($this->order)) ? " ORDER BY " . implode(",", $this->order) : ""; $limit = $this->buildLimit(); - $this->sql = "{$this->explain}SELECT $this->noCache$this->calRows$this->distinct$columns FROM " . $this->getTable(true) . "$join$where$this->group$having$order$limit$this->union"; - return $this; } @@ -221,7 +222,7 @@ protected function selectView(): self protected function insert(): self { $this->sql = "{$this->explain}INSERT INTO " . $this->getTable() . " " . - $this->buildInsertSet() . $this->buildDuplicate(); + $this->buildInsertSet() . $this->buildDuplicate() . $this->buildReturning(); return $this; } @@ -236,7 +237,7 @@ protected function update(): self $limit = $this->buildLimit(); $this->sql = "{$this->explain}UPDATE " . $this->getTable() . "$join SET " . - $this->buildUpdateSet() . "$where$limit}"; + $this->buildUpdateSet() . "$where$limit}" . $this->buildReturning(); return $this; } @@ -526,6 +527,17 @@ public function group(...$columns): self return $this; } + /** + * Postgre specific function + * @param string $column + * @return $this + */ + public function returning(string $column): self + { + $this->returning = (string)$this->prep($column); + return $this; + } + /** * Mysql JOIN query (Default: INNER) * @param string|array|MigrateInterface $table Mysql table name (if array e.g. [TABLE_NAME, ALIAS]) or MigrateInterface instance @@ -550,7 +562,7 @@ public function join( throw new DBQueryException("You need to specify the argument 2 (where) value!", 1); } - $prefix = Connect::getInstance()->getHandler()->getPrefix(); + $prefix = $this->connInst()->getHandler()->getPrefix(); $arr = $this->sperateAlias($table); $table = (string)$this->prep($arr['table'], false); $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " $table"; @@ -737,6 +749,19 @@ private function buildUpdateSet(?array $arr = null): string return implode(",", $new); } + /** + * Will build a returning value that can be fetched with insert id + * This is a PostgreSQL specific function. + * @return string + */ + private function buildReturning(): string + { + if(!is_null($this->returning) && $this->connInst()->getHandler()->getType() === "postgresql") { + return " RETURNING $this->returning"; + } + return ""; + } + /** * Build on duplicate sql string part * @return string @@ -797,7 +822,7 @@ private function buildLimit(): string /** * Used to call method that builds SQL queries - * @throws DBQueryException + * @throws DBQueryException|DBValidationException */ final protected function build(): void { @@ -816,7 +841,7 @@ final protected function build(): void /** * Generate SQL string of current instance/query * @return string - * @throws DBQueryException + * @throws DBQueryException|DBValidationException */ public function sql(): string { @@ -824,42 +849,22 @@ public function sql(): string return $this->sql; } - /** - * Start Transaction - * @return mixed - * @throws ConnectException - */ - public static function transaction(): mixed - { - return Connect::getInstance()->getHandler()->transaction(); - } - - /** - * Get return a new generated UUID - * DEPRECATED: Will be moved to Connect for starter - * @throws ConnectException|DBQueryException - */ - public static function getUUID(): ?string - { - $result = Connect::getInstance()->query("SELECT UUID()"); - if (is_object($result)) { - if ($result->num_rows > 0) { - $row = $result->fetch_row(); - return ($row[0] ?? null); - } - return null; - } else { - throw new DBQueryException(Connect::getInstance()->DB()->error, 1); - } - } - /** * Get insert AI ID from prev inserted result * @return int|string - * @throws ConnectException + * @throws ConnectException|DBQueryException */ public function insertID(): int|string { - return Connect::getInstance()->DB()->insert_id; + if($this->connInst()->getHandler()->getType() === "postgresql") { + if(is_null($this->returning)) { + throw new DBQueryException("You need to specify the returning column when using PostgreSQL."); + } + return $this->connInst()->DB()->insert_id($this->returning); + } + if($this->connInst()->getHandler()->getType() === "sqlite") { + return $this->connInst()->DB()->lastInsertRowID(); + } + return $this->connInst()->DB()->insert_id; } } \ No newline at end of file diff --git a/Handlers/PostgreSQL/PostgreSQLConnect.php b/Handlers/PostgreSQL/PostgreSQLConnect.php new file mode 100755 index 0000000..bc3fc4a --- /dev/null +++ b/Handlers/PostgreSQL/PostgreSQLConnect.php @@ -0,0 +1,106 @@ +connection = pg_connect("host=$server port=$port dbname=$dbname user=$user password=$pass"); + if (!$this->connection) { + $this->error = pg_last_error(); + } + } + + public function getConnection(): Connection + { + return $this->connection; + } + + /** + * Returns Connection of PgSql\Connection + * @param string $name + * @param array $arguments + * @return Connection|false + */ + public function __call(string $name, array $arguments): Connection|false + { + return call_user_func_array([$this->connection, $name], $arguments); + } + + /** + * Query sql + * @param $sql + * @return PostgreSQLResult|bool + */ + function query($sql): PostgreSQLResult|bool + { + if($this->connection instanceof Connection) { + $this->query = new PostgreSQLResult($this->connection); + if($query = $this->query->query($sql)) { + return $query; + } + $this->error = pg_result_error($this->connection); + } + return false; + } + + /** + * Begin transaction + * @return bool + */ + function begin_transaction(): bool + { + return (bool)$this->query("BEGIN"); + } + + /** + * Commit transaction + * @return bool + */ + function commit(): bool + { + return (bool)$this->query("COMMIT"); + } + + /** + * Rollback transaction + * @return bool + */ + function rollback(): bool + { + return (bool)$this->query("ROLLBACK"); + } + + /** + * Get insert ID + * @return mixed + */ + function insert_id(?string $column = null): int + { + return (int)pg_fetch_result($this->query, 0, $column); + } + + /** + * Close the connection + * @return void + */ + function close(): void + { + pg_close($this->connection); + } +} diff --git a/Handlers/PostgreSQL/PostgreSQLResult.php b/Handlers/PostgreSQL/PostgreSQLResult.php new file mode 100755 index 0000000..ecfd402 --- /dev/null +++ b/Handlers/PostgreSQL/PostgreSQLResult.php @@ -0,0 +1,106 @@ +connection = $connection; + } + + /** + * Get query + * @param $sql + * @return false|$this|self + */ + public function query($sql): self|false + { + if($this->query = pg_query($this->connection, $sql)) { + $this->num_rows = pg_affected_rows($this->query); + return $this; + } + return false; + } + + /** + * Fetch the next row of a result set as an object + * @param string $class + * @param array $constructor_args + * @return object|false|null + */ + public function fetch_object(string $class = "stdClass", array $constructor_args = []): object|false|null + { + return pg_fetch_object($this->query, null, $class, $constructor_args); + } + + /** + * Fetch the next row of a result set as an associative, a numeric array, or both + * @param int $mode Should be the database default const mode value e.g. MYSQLI_BOTH|PGSQL_BOTH|SQLITE3_BOTH + * @return array|false|null + */ + public function fetch_array(int $mode = PGSQL_BOTH): array|false|null + { + return pg_fetch_array($this->query, null, $mode); + } + + /** + * Fetch the next row of a result set as an associative array + * @return array|false|null + */ + public function fetch_assoc(): array|false|null + { + return pg_fetch_assoc($this->query); + } + + /** + * Fetch the next row of a result set as an enumerated array + * @return array|false|null + */ + public function fetch_row(): array|false|null + { + return pg_fetch_row($this->query); + } + + /** + * Frees the memory associated with a result + * free() and close() are aliases for free_result() + * @return void + */ + public function free(): void + { + $this->free_result(); + } + + /** + * Frees the memory associated with a result + * free() and close() are aliases for free_result() + * @return void + */ + public function close(): void + { + $this->free_result(); + } + + /** + * Frees the memory associated with a result + * free() and close() are aliases for free_result() + * @return void + */ + public function free_result(): void + { + pg_free_result($this->query); + } +} diff --git a/Handlers/PostgreSQLHandler.php b/Handlers/PostgreSQLHandler.php new file mode 100755 index 0000000..0718f24 --- /dev/null +++ b/Handlers/PostgreSQLHandler.php @@ -0,0 +1,192 @@ +server = $server; + $this->user = $user; + $this->pass = $pass; + $this->dbname = $dbname; + $this->port = $port; + } + + /** + * Get database type + * @return string + */ + public function getType(): string + { + return "postgresql"; + } + + /** + * Set MySql charset + * @param string $charset + */ + public function setCharset(string $charset): void + { + $this->charset = $charset; + } + + /** + * Set table prefix + * @param string $prefix + */ + public function setPrefix(string $prefix): void + { + if (strlen($prefix) > 0 && !str_ends_with($prefix, "_")) { + throw new InvalidArgumentException("The Prefix has to end with a underscore e.g. (prefix\"_\")!", 1); + } + $this->prefix = $prefix; + } + + /** + * Check if a connections is open + * @return bool + */ + public function hasConnection(): bool + { + return ($this->connection instanceof Connection); + } + + /** + * Connect to database + * @return PostgreSQLConnect + * @throws ConnectException + */ + public function execute(): PostgreSQLConnect + { + + $this->connection = new PostgreSQLConnect($this->server, $this->user, $this->pass, $this->dbname, $this->port); + if (!is_null($this->connection->error)) { + throw new ConnectException('Failed to connect to PostgreSQL: ' . $this->connection->error, 1); + } + $encoded = pg_set_client_encoding($this->connection->getConnection(), $this->charset); + if ($encoded < 0) { + throw new ConnectException("Error loading character set " . $this->charset, 2); + } + return $this->connection; + } + + /** + * Get selected database name + * @return string + */ + public function getDBName(): string + { + return $this->dbname; + } + + /** + * Get current Character set + * @return string|null + */ + public function getCharSetName(): ?string + { + return $this->charSetName; + } + + /** + * Get current table prefix + * @return string + */ + public function getPrefix(): string + { + return $this->prefix; + } + + /** + * Query sql string + * @param string $sql + * @return object|bool + */ + public function query(string $sql): object|bool + { + $result = new PostgreSQLResult($this->connection->getConnection()); + return $result->query($sql); + } + + + /** + * Close MySQL Connection + * @return void + * @throws ConnectException + */ + public function close(): void + { + if(!pg_close($this->connection->getConnection())) { + throw new ConnectException("Failed to close pgsql connection:" . pg_last_error($this->connection->getConnection()), 1); + } + } + + + /** + * Protect/prep database values from injections + * @param string $value + * @return string + */ + public function prep(string $value): string + { + return pg_escape_string($this->connection->getConnection(), $value); + } + + /** + * Start Transaction + * @return PostgreSQLConnect + */ + public function transaction(): PostgreSQLConnect + { + $this->connection->begin_transaction(); + return $this->connection; + } + + /** + * Execute multiple queries at once (e.g. from a sql file) + * @param string $sql + * @param object|null &$db + * @return array + */ + public function multiQuery(string $sql, object &$db = null): array + { + $count = 0; + $err = array(); + $db = $this->connection->getConnection(); + // Split the SQL string into individual queries + $queries = explode(';', $sql); + // Loop through each query and execute it + foreach ($queries as $query) { + $query = trim($query); // Clean up whitespace + if (empty($query)) { + continue; // Skip empty queries + } + $result = pg_query($db, $query); + if (!$result) { + $err[$count] = pg_last_error($db); + break; // Stop on the first error + } + $count++; + } + return $err; + } +} diff --git a/Handlers/SQLite/SQLiteConnect.php b/Handlers/SQLite/SQLiteConnect.php new file mode 100644 index 0000000..3c1f0f1 --- /dev/null +++ b/Handlers/SQLite/SQLiteConnect.php @@ -0,0 +1,96 @@ +connection = new SQLite3($database); + + } catch (Exception $e) { + throw new ConnectException('Failed to connect to SQLite: ' . $e->getMessage(), 1); + } + return $this->connection; + } + + /** + * Access the database main class + * @param string $method + * @param array $arguments + * @return object|false + */ + public function __call(string $method, array $arguments): SQLite3|false + { + return call_user_func_array([$this->connection, $method], $arguments); + } + + /** + * Performs a query on the database + * @param string $query + * @return object|false + */ + public function query(string $query): SQLiteResult|false + { + $result = new SQLiteResult($this->connection); + if($this->query = $result->query($query)) { + return $this->query; + } + $this->error = $this->connection->lastErrorMsg(); + + //$this->query = parent::query($query); + return false; + } + + /** + * Begin transaction + * @return bool + */ + function begin_transaction(): bool + { + return (bool)$this->query("BEGIN TRANSACTION"); + } + + /** + * Commit transaction + * @return bool + */ + function commit(): bool + { + return (bool)$this->query("COMMIT"); + } + + /** + * Rollback transaction + * @return bool + */ + function rollback(): bool + { + return (bool)$this->query("ROLLBACK"); + } + + /** + * Returns the value generated for an AI column by the last query + * @param string|null $column Is only used with PostgreSQL! + * @return int + */ + function insert_id(?string $column = null): int + { + return $this->connection->lastInsertRowID(); + } + +} \ No newline at end of file diff --git a/Handlers/SQLite/SQLiteResult.php b/Handlers/SQLite/SQLiteResult.php new file mode 100644 index 0000000..7454f8f --- /dev/null +++ b/Handlers/SQLite/SQLiteResult.php @@ -0,0 +1,193 @@ +connection = $connection; + } + + /** + * Get query + * @param $sql + * @return false|$this|self + */ + public function query($sql): self|false + { + if($this->query = $this->connection->query($sql)) { + $this->preFetchData(); + return $this; + } + return false; + } + + /** + * Fetch the next row of a result set as an object + * @param string $class + * @param array $constructor_args + * @return object|false|null + * @throws ReflectionException + */ + public function fetch_object(string $class = "stdClass", array $constructor_args = []): object|false|null + { + if(!$this->startIndex()) { + return false; + } + $data = $this->rowsObj[$this->index] ?? false; + if ($class !== 'stdClass' && is_object($data)) { + $data = $this->bindToClass($data, $class, $constructor_args); + } + $this->endIndex(); + + return $data; + } + + /** + * Fetch the next row of a result set as an associative, a numeric array, or both + * @param int $mode Should be the database default const mode value e.g. MYSQLI_BOTH|PGSQL_BOTH|SQLITE3_BOTH + * @return array|false|null + */ + public function fetch_array(int $mode = PGSQL_BOTH): array|false|null + { + if($mode !== SQLITE3_ASSOC) { + return $this->query->fetchArray($mode); + } + if(!$this->startIndex()) { + return false; + } + $data = $this->rows[$this->index] ?? false; + $this->endIndex(); + return $data; + + } + + /** + * Fetch the next row of a result set as an associative array + * @return array|false|null + */ + public function fetch_assoc(): array|false|null + { + if(!$this->startIndex()) { + return false; + } + $data = $this->rows[$this->index] ?? false; + $this->endIndex(); + return $data; + } + + /** + * Fetch the next row of a result set as an enumerated array + * @return array|false|null + */ + public function fetch_row(): array|false|null + { + return $this->query->fetchArray(SQLITE3_NUM); + } + + /** + * Frees the memory associated with a result + * free() and close() are aliases for free_result() + * @return void + */ + public function free(): void + { + } + + /** + * Frees the memory associated with a result + * free() and close() are aliases for free_result() + * @return void + */ + public function close(): void + { + } + + /** + * Frees the memory associated with a result + * free() and close() are aliases for free_result() + * @return void + */ + public function free_result(): void + { + } + + /** + * This will prepare the fetch data with information + * @return void + */ + protected function preFetchData(): void + { + $result = $this->query->fetchArray(SQLITE3_ASSOC); + if($result !== false) { + $this->rowsObj = $this->rows = []; + $this->num_rows = 0; + while ($row = $this->query->fetchArray(SQLITE3_ASSOC)) { + $this->rows[] = $row; + $this->rowsObj[] = (object)$row; + $this->num_rows++; + } + } + } + + /** + * Start the indexing + * @return bool + */ + protected function startIndex(): bool + { + if(($this->rows === false)) { + return false; + } + $this->index++; + return true; + } + + /** + * End the indexing and clean up + * @return void + */ + protected function endIndex(): void + { + if($this->index >= $this->num_rows) { + $this->index = 0; + } + } + + /** + * Bind object to a class + * @param object|array $data + * @param string $class + * @param array $constructor_args + * @return object|string|null + * @throws ReflectionException + */ + protected function bindToClass(object|array $data, string $class, array $constructor_args = []): object|string|null + { + $reflection = new ReflectionClass($class); + $object = $reflection->newInstanceArgs($constructor_args); + foreach ($data as $key => $value) { + if (property_exists($object, $key) && is_null($object->{$key})) { + $object->{$key} = $value; + } + } + return $object; + } +} diff --git a/Handlers/SQLiteHandler.php b/Handlers/SQLiteHandler.php index 1a35623..5cb96a0 100755 --- a/Handlers/SQLiteHandler.php +++ b/Handlers/SQLiteHandler.php @@ -7,8 +7,9 @@ use InvalidArgumentException; use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\HandlerInterface; +use MaplePHP\Query\Handlers\SQLite\SQLiteConnect; use SQLite3; -use SQLite3Result; +use MaplePHP\Query\Handlers\SQLite\SQLiteResult; class SQLiteHandler implements HandlerInterface { @@ -16,7 +17,7 @@ class SQLiteHandler implements HandlerInterface private ?string $charSetName = null; private string $charset = "UTF-8"; private string $prefix = ""; - private ?SQLite3 $connection; + private ?SQLiteConnect $connection; public function __construct(string $database) { @@ -68,13 +69,13 @@ public function hasConnection(): bool /** * Connect to database - * @return SQLite3 + * @return SQLiteConnect * @throws ConnectException */ - public function execute(): SQLite3 + public function execute(): SQLiteConnect { try { - $this->connection = new SQLite3($this->database); + $this->connection = new SQLiteConnect($this->database); } catch (Exception $e) { throw new ConnectException('Failed to connect to SQLite: ' . $e->getMessage(), 1); @@ -112,9 +113,9 @@ public function getPrefix(): string /** * Query sql string * @param string $sql - * @return SQLite3Result|false + * @return SQLiteResult|false */ - public function query(string $sql): bool|SQLite3Result + public function query(string $sql): bool|SQLiteResult { return $this->connection->query($sql); } @@ -135,7 +136,7 @@ public function close(): void */ public function prep(string $value): string { - return SQLite3::escapeString($value); + return SQLiteConnect::escapeString($value); } /** @@ -166,11 +167,11 @@ public function multiQuery(string $sql, object &$db = null): array /** * Start Transaction - * @return SQLite3 + * @return SQLiteConnect */ - public function transaction(): SQLite3 + public function transaction(): SQLiteConnect { - $this->connection->exec('BEGIN TRANSACTION'); + $this->connection->begin_transaction(); return $this->connection; } } diff --git a/Interfaces/ConnectInterface.php b/Interfaces/ConnectInterface.php new file mode 100644 index 0000000..3097eac --- /dev/null +++ b/Interfaces/ConnectInterface.php @@ -0,0 +1,50 @@ +sql = $sql; if ($sql instanceof DBInterface) { $this->sql = $sql->sql(); } + $this->connection = is_null($connection) ? Connect::getInstance() : $connection; } public function setPluck(?string $pluck): void { $this->pluck = $pluck; } + /** * Execute query result * @return object|array|bool @@ -30,16 +33,17 @@ public function setPluck(?string $pluck): void */ public function execute(): object|array|bool { - if ($result = Connect::getInstance()->query($this->sql)) { + if ($result = $this->connection->query($this->sql)) { return $result; } else { - throw new ConnectException(Connect::getInstance()->DB()->error, 1); + throw new ConnectException($this->connection->DB()->error, 1); } } /** - * Execute query result And fetch as obejct + * Execute query result And fetch as object * @return bool|object|string + * @throws ConnectException */ public function get(): bool|object|string { @@ -51,11 +55,11 @@ public function get(): bool|object|string * @return bool|object|string (Mysql result) * @throws ConnectException */ - final public function obj(): bool|object|string + final public function obj(string $class = "stdClass", array $constructor_args = []): bool|object|string { $result = $this->execute(); if (is_object($result) && $result->num_rows > 0) { - $obj = $result->fetch_object(); + $obj = $result->fetch_object($class, $constructor_args); if(!is_null($this->pluck)) { $obj = $obj->{$this->pluck}; } @@ -70,14 +74,14 @@ final public function obj(): bool|object|string * @return array * @throws ConnectException */ - final public function fetch(?callable $callback = null): array + final public function fetch(?callable $callback = null, string $class = "stdClass", array $constructor_args = []): array { $key = 0; $select = null; $arr = array(); $result = $this->execute(); if (is_object($result) && $result->num_rows > 0) { - while ($row = $result->fetch_object()) { + while ($row = $result->fetch_object($class, $constructor_args)) { if(!is_null($this->pluck)) { $row = $row->{$this->pluck}; @@ -93,6 +97,7 @@ final public function fetch(?callable $callback = null): array } $arr = array_replace_recursive($arr, $select); } else { + $arr[$data] = $row; } @@ -109,6 +114,6 @@ final public function fetch(?callable $callback = null): array */ public function insertID(): string|int { - return Connect::getInstance()->DB()->insert_id; + return $this->connection->DB()->insert_id; } } diff --git a/README.md b/README.md index 758160f..e0cfcdf 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MaplePHP - Database query builder -MaplePHP - Database query builder is a powerful yet **user-friendly** library for making **safe** database queries, with support for both MySQL and SQLite. +MaplePHP - Database query builder is a powerful yet **user-friendly** library for making **safe** database queries, with support for MySQL, SQLite and PostgreSQL. ### Contents - [Connect to the database](#connect-to-the-database) @@ -20,17 +20,13 @@ $handler = new MySQLHandler( $user, $password, $databaseName, - $port + $port = 3306 ); -// Maple DB do also support SQLite. -//$handler = new SQLiteHandler("database.SQLite"); - // Recommend: Set TABLE prefix. This will make your life easier -// MaplePHP DB class will "automatically prepend" it to the table names. +// MaplePHP will automatically prepend the prefix to the table names. $handler->setPrefix("maple_"); $handler->setCharset("utf8mb4"); - $connect = Connect::setHandler($handler); $connect->execute(); ``` @@ -45,6 +41,16 @@ $connect = Connect::setHandler($SQLiteHandler); $connect->execute(); ``` +### Connect to PostgreSQL +```php +use MaplePHP\Query\Connect; + +$postgreSQLHandler = new PostgreSQLHandler($server, $user, $password, $databaseName, $port = 5432); +$postgreSQLHandler->setPrefix("mp_"); +$connect = Connect::setHandler($postgreSQLHandler); +$connect->execute(); +``` + ***Note:** That the first connection will count as the main connection if not overwritten. You can also have multiple connection, [click here](#multiple-connections) for more information.* @@ -150,7 +156,7 @@ $select->joinCross("tableName", ["b.user_id" => "a.id"]); ### Pluck ```php -$pluck->pluck("a.name")->get(); +$select->pluck("a.name")->get(); ``` From 5750297c83ef117c06c69e49b6203096431a0513 Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Sun, 18 Aug 2024 12:37:26 +0200 Subject: [PATCH 15/17] Bump version to v2.0.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1aeae91..69b761c 100644 --- a/composer.json +++ b/composer.json @@ -34,5 +34,5 @@ } }, "minimum-stability": "dev", - "version": "v2.0.0" + "version": "v2.0.1" } From fa34895c2801c37cab75dddefa0c680a5a3fcebd Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Sun, 18 Aug 2024 14:25:24 +0200 Subject: [PATCH 16/17] Fix SQLite indexing --- AbstractMigrate.php | 4 +- Handlers/SQLite/SQLiteResult.php | 25 +++-- tests/unitary-db.php | 177 ++++++++++++++++--------------- 3 files changed, 111 insertions(+), 95 deletions(-) diff --git a/AbstractMigrate.php b/AbstractMigrate.php index ba698eb..d98ed0f 100755 --- a/AbstractMigrate.php +++ b/AbstractMigrate.php @@ -16,12 +16,14 @@ abstract protected function migrateTable(); public function __construct(string $table, ?string $prefix = null) { - if (is_null($prefix)) { + /* + if (is_null($prefix)) { $prefix = getenv("MYSQL_PREFIX"); if ($prefix === false) { throw new Exception("Table prefix is required!", 1); } } + */ $this->mig = new Create($table, $prefix); $this->table = $table; } diff --git a/Handlers/SQLite/SQLiteResult.php b/Handlers/SQLite/SQLiteResult.php index 7454f8f..3eacd81 100644 --- a/Handlers/SQLite/SQLiteResult.php +++ b/Handlers/SQLite/SQLiteResult.php @@ -12,7 +12,7 @@ class SQLiteResult { - public $index = 0; + public $index = -1; public int|string $num_rows = 0; public array|bool $rows = false; public array|bool $rowsObj = false; @@ -135,15 +135,18 @@ public function free_result(): void */ protected function preFetchData(): void { - $result = $this->query->fetchArray(SQLITE3_ASSOC); - if($result !== false) { - $this->rowsObj = $this->rows = []; - $this->num_rows = 0; - while ($row = $this->query->fetchArray(SQLITE3_ASSOC)) { - $this->rows[] = $row; - $this->rowsObj[] = (object)$row; - $this->num_rows++; - } + $this->rowsObj = $this->rows = []; + $this->num_rows = 0; + $obj = $arr = array(); + while ($row = $this->query->fetchArray(SQLITE3_ASSOC)) { + $arr[] = $row; + $obj[] = (object)$row; + $this->num_rows++; + } + + if(count($arr) > 0) { + $this->rows = $arr; + $this->rowsObj = $obj; } } @@ -167,7 +170,7 @@ protected function startIndex(): bool protected function endIndex(): void { if($this->index >= $this->num_rows) { - $this->index = 0; + $this->index = -1; } } diff --git a/tests/unitary-db.php b/tests/unitary-db.php index 320584c..c42546c 100755 --- a/tests/unitary-db.php +++ b/tests/unitary-db.php @@ -2,93 +2,104 @@ use database\migrations\Test; use database\migrations\TestCategory; -use MaplePHP\Query\Exceptions\ConnectException; -use MaplePHP\Query\Exceptions\DBQueryException; -use MaplePHP\Query\Exceptions\DBValidationException; +use MaplePHP\Query\Handlers\PostgreSQLHandler; use MaplePHP\Query\Handlers\SQLiteHandler; use MaplePHP\Unitary\Unit; use MaplePHP\Query\Connect; use MaplePHP\Query\DB; // Only validate if there is a connection open! -try { - if (Connect::hasInstance() && Connect::getInstance()->hasConnection()) { - $unit = new Unit(); - - // Add a title to your tests (not required) - $unit->addTitle("Testing MaplePHP Query library!"); - $unit->add("MySql Query builder", function ($inst) { - - $db = Connect::getInstance(); - $select = $db::select("id,a.name,b.name AS cat", ["test", "a"])->whereParent(0)->where("status", 0, ">")->limit(6); - $select->join(["test_category", "b"], "tid = id"); - - // 3 queries - $obj = $select->get(); - $arr = $select->fetch(); - $pluck = DB::table("test")->pluck("name")->get(); - - $inst->add($obj, [ - "isObject" => [], - "missingColumn" => function () use ($obj) { - return (isset($obj->name) && isset($obj->cat)); - } - ], "Data is missing"); - - $inst->add($arr, [ - "isArray" => [], - "noRows" => function () use ($arr) { - return (count($arr) > 0); - } - ], "Fetch feed empty"); - - $inst->add($pluck, [ - "isString" => [], - "length" => [1] - ], "Pluck is expected to return string"); - - $select = $db::select("id,test.name,test_category.name AS cat", new Test)->whereParent(0)->where("status", 0, ">")->limit(6); - $select->join(new TestCategory); - $obj = $select->obj(); - - $inst->add($obj, [ - "isObject" => [], - "missingColumn" => function () use ($obj) { - return (isset($obj->name) && isset($obj->cat)); - } - ], "Data is missing"); - }); - - /** - * This will test multiple databases AND - * validate sqLite database - */ - $unit->add("sqLite Query builder", function ($inst) { - - $sqLiteHandler = new SQLiteHandler(__DIR__ . "/database.sqlite"); - $sqLiteHandler->setPrefix("mp_"); - $connect = Connect::setHandler($sqLiteHandler, "mp"); - $connect->execute(); - - // Access sqLite connection - $select = Connect::getInstance("mp")::select("id,name,content", "test")->whereStatus(1)->limit(3); - $result = $select->fetch(); - $inst->add($select->fetch(), [ - "isArray" => [], - "rows" => function () use ($result) { - return (count($result) === 3); - } - ], "Fetch should equal to 3"); - }); - - $unit->execute(); - } - -} catch (ConnectException|DBValidationException|DBQueryException $e) { - echo $e->getMessage(); +if (Connect::hasInstance() && Connect::getInstance()->hasConnection()) { + + $unit = new Unit(); + + // Add a title to your tests (not required) + $unit->addTitle("Testing MaplePHP Query library!"); + $unit->add("MySql Query builder", function ($inst) { + + $db = Connect::getInstance(); + $select = $db::select("id,a.name,b.name AS cat", ["test", "a"])->whereParent(0)->where("status", 0, ">")->limit(6); + $select->join(["test_category", "b"], "tid = id"); + + // 3 queries + $obj = $select->get(); + $arr = $select->fetch(); + $pluck = DB::table("test")->pluck("name")->get(); + + $inst->add($obj, [ + "isObject" => [], + "missingColumn" => function () use ($obj) { + return (isset($obj->name) && isset($obj->cat)); + } + ], "Data is missing"); + + $inst->add($arr, [ + "isArray" => [], + "noRows" => function () use ($arr) { + return (count($arr) > 0); + } + ], "Fetch feed empty"); + + $inst->add($pluck, [ + "isString" => [], + "length" => [1] + ], "Pluck is expected to return string"); + + $select = $db::select("id,test.name,test_category.name AS cat", new Test)->whereParent(0)->where("status", 0, ">")->limit(6); + $select->join(new TestCategory); + $obj = $select->obj(); + + $inst->add($obj, [ + "isObject" => [], + "missingColumn" => function () use ($obj) { + return (isset($obj->name) && isset($obj->cat)); + } + ], "Data is missing"); + }); + + /** + * This will test multiple databases AND + * validate sqLite database + */ + $unit->add("sqLite Query builder", function ($inst) { + + $sqLiteHandler = new SQLiteHandler(__DIR__ . "/database.sqlite"); + $sqLiteHandler->setPrefix("mp_"); + $connect = Connect::setHandler($sqLiteHandler, "lite"); + $connect->execute(); + + // Access sqLite connection + $select = Connect::getInstance("lite")::select("id,name,content", "test")->whereStatus(1)->limit(3); + $result = $select->fetch(); + $inst->add($result, [ + "isArray" => [], + "rows" => function () use ($result) { + return (count($result) === 3); + } + ], "Fetch should equal to 3"); + }); + + /** + * This will test multiple databases AND + * validate sqLite database + */ + $unit->add("sqLite Query builder", function ($inst) { + + $sqLiteHandler = new PostgreSQLHandler("127.0.0.1", "postgres", "", "maplephp"); + $sqLiteHandler->setPrefix("maple_"); + $connect = Connect::setHandler($sqLiteHandler, "psg"); + $connect->execute(); + + // Access sqLite connection + $select = Connect::getInstance("psg")::select("id,name", ["test", "a"])->limit(2); + $result = $select->fetch(); + $inst->add($result, [ + "isArray" => [], + "rows" => function () use ($result) { + return (count($result) === 2); + } + ], "Fetch should equal to 2"); + }); + + $unit->execute(); } - - - - - From c7d370d5636231eb517ef3572280a6bb155f3a5d Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Sun, 18 Aug 2024 14:25:49 +0200 Subject: [PATCH 17/17] Bump version to v2.0.2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 69b761c..9df6b57 100644 --- a/composer.json +++ b/composer.json @@ -34,5 +34,5 @@ } }, "minimum-stability": "dev", - "version": "v2.0.1" + "version": "v2.0.2" }