diff --git a/AbstractDB.php b/AbstractDB.php index 9130a2c..e25cfbe 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; @@ -27,11 +27,13 @@ abstract class AbstractDB implements DBInterface protected $mig; protected $compare = "="; protected $whereAnd = "AND"; + protected $whereNot = false; protected $whereIndex = 0; protected $whereProtocol = []; protected $fkData; protected $joinedTables; + protected string $connKey = "default"; /** * Build SELECT sql code (The method will be auto called in method build) @@ -83,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::DB(); + return $this->connInst()->DB(); } /** @@ -99,7 +109,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 $this->connInst()->getHandler()->getPrefix() . $this->table . $alias; } /** @@ -108,6 +118,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); } @@ -215,10 +228,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 @@ -241,7 +293,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 @@ -304,51 +357,20 @@ 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; - } - - /** - * 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; + 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 { $joinArr = array(); - $prefix = Connect::prefix(); + $prefix = $this->connInst()->getHandler()->getPrefix(); $main = $this->getMainFKData(); $data = $mig->getData(); $this->mig->mergeData($data); @@ -358,16 +380,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)"; } } } @@ -378,7 +400,6 @@ final protected function buildJoinFromMig(MigrateInterface $mig, string $type): return $joinArr; } - /** * Build on YB to col sql string part * @return string|null @@ -395,19 +416,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 = new Query($sql, $this->connInst()); + $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/AbstractMigrate.php b/AbstractMigrate.php index 7a9b9fb..d98ed0f 100755 --- a/AbstractMigrate.php +++ b/AbstractMigrate.php @@ -3,24 +3,27 @@ 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)) { + /* + 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); $this->table = $table; } @@ -31,7 +34,7 @@ public function __construct(string $table, ?string $prefix = null) */ public function getBuild(): Create { - $this->buildTable(); + $this->migrateTable(); return $this->mig; } @@ -60,7 +63,7 @@ public function drop(): array */ public function getData(): array { - $this->buildTable(); + $this->migrateTable(); return $this->mig->getColumns(); } @@ -71,10 +74,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 +85,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 121aa24..9eb0bef 100755 --- a/Connect.php +++ b/Connect.php @@ -3,112 +3,171 @@ namespace MaplePHP\Query; +use InvalidArgumentException; 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 $columns, string|array|MigrateInterface $table) + * @method static table(string $string) + */ class Connect { - private $server; - private $user; - private $pass; - private $dbname; - private $charSetName; - private $charset = "utf8mb4"; - private static $self; - private static $prefix; - private static $selectedDB; - private static $mysqlVars; + private $handler; + private static array $inst; + public static string $current = "default"; + private $db; - public function __construct($server, $user, $pass, $dbname) + private function __construct($handler) { - $this->server = $server; - $this->user = $user; - $this->pass = $pass; - $this->dbname = $dbname; - 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 + * Access query builder instance + * @param string $name + * @param array $arguments + * @return mixed */ - public function setCharset(string $charset): void + public static function __callStatic(string $name, array $arguments) { - $this->charset = $charset; + $inst = new DB(); + $inst->setConnKey(self::$current); + return $inst::$name(...$arguments); } /** - * Set table prefix - * @param string $prefix + * Set connection handler + * @param $handler + * @param string|null $key + * @return self */ - public static function setPrefix(string $prefix): void + public static function setHandler($handler, ?string $key = null): self { - if (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)) { + throw new InvalidArgumentException("A handler is already connected with key \"$key\"!"); } - self::$prefix = $prefix; + self::$inst[$key] = new self($handler); + return self::$inst[$key]; } /** - * Connect to database + * Remove a handler + * @param string $key * @return void */ - public function execute(): void + public static function removeHandler(string $key): void { - self::$selectedDB = new mysqli($this->server, $this->user, $this->pass, $this->dbname); - if (mysqli_connect_error()) { - throw new ConnectException('Failed to connect to MySQL: ' . mysqli_connect_error(), 1); + 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!"); } - if (!mysqli_set_charset(self::$selectedDB, $this->charset)) { - throw new ConnectException("Error loading character set " . $this->charset . ": " . mysqli_error(self::$selectedDB), 2); + unset(self::$inst[$key]); + if(self::$current === $key) { + self::$current = "default"; } - $this->charSetName = mysqli_character_set_name(self::$selectedDB); } /** - * Get current DB connection + * Get default instance or secondary instances with key + * @param string|null $key + * @return self + * @throws ConnectException */ - public static function DB(): mysqli + public static function getInstance(?string $key = null): self { - return static::$selectedDB; + $key = self::getKey($key); + if(!self::hasInstance($key)) { + throw new ConnectException("Connection Error: No active connection or connection instance found."); + } + self::$current = $key; + return self::$inst[$key]; } /** - * Get selected database name - * @return string + * Check if default instance or secondary instances exist for key + * @param string|null $key + * @return bool */ - public function getDBName(): string + public static function hasInstance(?string $key = null): bool { - return $this->dbname; + $key = self::getKey($key); + return (isset(self::$inst[$key]) && (self::$inst[$key] instanceof self)); } /** - * Get current Character set - * @return string|null + * Get the possible connection key + * @param string|null $key + * @return string */ - public function getCharSetName(): ?string + private static function getKey(?string $key = null): string { - return $this->charSetName; + $key = (is_null($key)) ? "default" : $key; + return $key; + } + + /** + * Access the connection handler + * @return mixed + */ + function getHandler() { + return $this->handler; } + /** + * Get database type + * @return string + */ + public function getType(): string + { + return $this->handler->getType(); + } /** * Get current table prefix * @return string */ - public static function getPrefix(): string + 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 + */ + public function execute(): void + { + $this->db = $this->handler->execute(); + } + + /** + * Get current DB connection + */ + public function DB(): mixed { - return static::$prefix; + return $this->db; } /** @@ -116,9 +175,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); } /** @@ -126,9 +185,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); } /** @@ -137,13 +196,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) @@ -151,82 +212,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..b805496 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()->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}'"); @@ -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 b00a3af..97714ef 100755 --- a/DB.php +++ b/DB.php @@ -3,14 +3,18 @@ namespace MaplePHP\Query; +use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\MigrateInterface; 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; @@ -31,20 +35,27 @@ class DB extends AbstractDB private $viewName; private $sql; private $dynamic; + private ?string $returning = null; + 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; $table = array_pop($args); $inst = self::table($table); $inst->method = $method; + $inst->setConnKey(Connect::$current); + $prefix = Connect::getInstance(Connect::$current)->getHandler()->getPrefix(); + switch ($inst->method) { case 'select': case 'selectView': @@ -57,39 +68,48 @@ 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]; break; } + } else { $inst = new self(); } + return $inst; } /** * 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": @@ -117,10 +137,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 { @@ -135,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; } @@ -143,19 +166,21 @@ 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; @@ -163,7 +188,9 @@ public static function withAttr(array|string|int|float $value, ?array $args = nu /** * Build SELECT sql code (The method will be auto called in method build) + * @method static __callStatic * @return self + * @throws DBValidationException */ protected function select(): self { @@ -173,16 +200,15 @@ 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}"; - + $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; } /** * Select view * @return self + * @throws DBValidationException */ protected function selectView(): self { @@ -196,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; } @@ -210,8 +236,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}" . $this->buildReturning(); return $this; } @@ -223,13 +249,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; } @@ -240,7 +266,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; } @@ -251,7 +277,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; } @@ -275,12 +301,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 @@ -313,9 +355,19 @@ 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 + * 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 @@ -325,18 +377,19 @@ 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; } /** * 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 { @@ -350,7 +403,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 @@ -366,10 +419,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 { @@ -382,7 +436,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 @@ -392,13 +446,13 @@ 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; } /** - * Add a limit and maybee a offset + * Add a limit and maybe an offset * @param int $limit * @param int|null $offset * @return self @@ -413,7 +467,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 */ @@ -425,9 +479,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 { @@ -437,13 +492,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 @@ -459,8 +514,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 { @@ -471,13 +527,27 @@ 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 - * @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, @@ -485,27 +555,32 @@ 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 { 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::prefix(); + $prefix = $this->connInst()->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(); 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); } } @@ -514,10 +589,9 @@ 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; } @@ -532,7 +606,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 @@ -563,14 +637,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); } @@ -578,9 +652,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 { @@ -605,7 +679,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); } @@ -644,6 +718,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 @@ -654,11 +729,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 @@ -668,11 +744,24 @@ private function buildUpdateSet(?array $arr = null): string } $new = array(); foreach ($arr as $key => $val) { - $new[] = "{$key} = {$val}"; + $new[] = "$key = $val"; } 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 @@ -688,19 +777,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++; @@ -719,7 +808,7 @@ private function buildJoin(): string } /** - * Byuld limit + * Build limit * @return string */ private function buildLimit(): string @@ -727,12 +816,13 @@ 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" : ""; } /** - * Used to call methoed that builds SQL queryies + * Used to call method that builds SQL queries + * @throws DBQueryException|DBValidationException */ final protected function build(): void { @@ -740,22 +830,18 @@ 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(); } } /** - * Genrate SQL string of current instance/query + * Generate SQL string of current instance/query * @return string + * @throws DBQueryException|DBValidationException */ public function sql(): string { @@ -763,66 +849,22 @@ 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 - * @return null|string - */ - public static function getUUID(): ?string - { - $result = Connect::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::DB()->error, 1); - } - } - /** * Get insert AI ID from prev inserted result * @return int|string + * @throws ConnectException|DBQueryException */ public function insertID(): int|string { - return Connect::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/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 new file mode 100755 index 0000000..ee25f28 --- /dev/null +++ b/Handlers/MySQLHandler.php @@ -0,0 +1,189 @@ +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; + } + + /** + * 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 + * @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/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..3eacd81 --- /dev/null +++ b/Handlers/SQLite/SQLiteResult.php @@ -0,0 +1,196 @@ +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 + { + $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; + } + } + + /** + * 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 = -1; + } + } + + /** + * 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 new file mode 100755 index 0000000..5cb96a0 --- /dev/null +++ b/Handlers/SQLiteHandler.php @@ -0,0 +1,177 @@ +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; + } + + /** + * 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 SQLiteConnect + * @throws ConnectException + */ + public function execute(): SQLiteConnect + { + try { + $this->connection = new SQLiteConnect($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 SQLiteResult|false + */ + public function query(string $sql): bool|SQLiteResult + { + 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 SQLiteConnect::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 SQLiteConnect + */ + public function transaction(): SQLiteConnect + { + $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 + * @throws ConnectException */ public function execute(): object|array|bool { - if ($result = Connect::query($this->sql)) { + if ($result = $this->connection->query($this->sql)) { return $result; } else { - throw new ConnectException(Connect::DB()->error, 1); + throw new ConnectException($this->connection->DB()->error, 1); } } /** - * Execute query result And fetch as obejct - * @return bool|object|array + * Execute query result And fetch as object + * @return bool|object|string + * @throws ConnectException */ - 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(string $class = "stdClass", array $constructor_args = []): bool|object|string { $result = $this->execute(); if (is_object($result) && $result->num_rows > 0) { - return $result->fetch_object(); + $obj = $result->fetch_object($class, $constructor_args); + if(!is_null($this->pluck)) { + $obj = $obj->{$this->pluck}; + } + return $obj; } return false; } /** * 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 + 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}; + } + if ($callback) { $select = $callback($row, $key); } @@ -77,6 +97,7 @@ final public function fetch(?callable $callback = null): array } $arr = array_replace_recursive($arr, $select); } else { + $arr[$data] = $row; } @@ -89,9 +110,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 $this->connection->DB()->insert_id; } } diff --git a/README.md b/README.md index c5b257a..e0cfcdf 100755 --- a/README.md +++ b/README.md @@ -1,23 +1,59 @@ -# 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. +# MaplePHP - Database query builder +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) +- [Make queries](#make-queries) +- [Attributes](#attributes) +- [Multiple Connections](#multiple-connections) +- *Migrations (Coming soon)* ## Connect to the database +### Connect to MySQL ```php use MaplePHP\Query\Connect; -$connect = new Connect($server, $user, $password, $databaseName); -$connect->setCharset("utf8mb4"); -// Recommened: Set TABLE prefix. This will make your life easier -// MaplePHP DB class will "automatically prepend" it to the table names. -$connect->setPrefix("maple_"); +$handler = new MySQLHandler( + $server, + $user, + $password, + $databaseName, + $port = 3306 +); + +// Recommend: Set TABLE prefix. This will make your life easier +// MaplePHP will automatically prepend the prefix to the table names. +$handler->setPrefix("maple_"); +$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(); +``` + +### 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.* + + ## Make queries Start with the namespace ```php @@ -26,10 +62,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); @@ -41,16 +79,20 @@ $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 -$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') @@ -63,7 +105,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,15 +130,36 @@ $select->order("id", "ASC")->order("parent", "DESC"); $select->orderRaw("id ASC, parent DESC"); // ORDER BY id ASC, parent DESC ``` -### Limit +### 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("tableName", "b.user_id = a.id"); // Default INNER join +$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->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"]); ``` + +### Pluck +```php +$select->pluck("a.name")->get(); +``` + + ### Insert ```php $insert = DB::insert("pages")->set(["id" => 36, "name" => "About us", "slug" => "about-us"])->onDupKey(); @@ -134,3 +197,89 @@ $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, 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 1b82913..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 @@ -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}"; 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/composer.json b/composer.json old mode 100755 new mode 100644 index 58ece73..9df6b57 --- a/composer.json +++ b/composer.json @@ -1,28 +1,38 @@ { - "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" + }, + "require-dev": { + "maplephp/unitary": "^1.0" + }, + "autoload": { + "psr-4": { + "MaplePHP\\Query\\": "" + } + }, + "minimum-stability": "dev", + "version": "v2.0.2" +} diff --git a/tests/database.sqlite b/tests/database.sqlite new file mode 100644 index 0000000..aa5c121 Binary files /dev/null and b/tests/database.sqlite differ diff --git a/tests/unitary-db.php b/tests/unitary-db.php new file mode 100755 index 0000000..c42546c --- /dev/null +++ b/tests/unitary-db.php @@ -0,0 +1,105 @@ +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(); +}