diff --git a/AbstractDB.php b/AbstractDB.php index e25cfbe..04b62bf 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -1,15 +1,17 @@ connKey = is_null($key) ? "default" : $key; } - - public function connInst() { + + /** + * @throws ConnectException + */ + public function connInst(): Connect + { return Connect::getInstance($this->connKey); } - + /** * Access Mysql DB connection - * @return \mysqli + * @return ConnectInterface + * @throws ConnectException */ - public function connect() + public function connect(): ConnectInterface { return $this->connInst()->DB(); } /** * Get current instance Table name with prefix attached + * @param bool $withAlias * @return string + * @throws ConnectException */ public function getTable(bool $withAlias = false): string { - $alias = ($withAlias && !is_null($this->alias)) ? " {$this->alias}" : ""; + $alias = ($withAlias && !is_null($this->alias)) ? " $this->alias" : ""; return $this->connInst()->getHandler()->getPrefix() . $this->table . $alias; } /** * Get current instance Columns * @return array + * @throws DBValidationException */ public function getColumns(): array { @@ -137,7 +154,6 @@ protected function getAttr(array|string|int|float $value): AttrInterface return new Attr($value); } - /** * Will reset Where input * @return void @@ -191,17 +207,18 @@ protected function joinTypes(string $val): string } /** - * Sperate Alias - * @param string|array $data + * Separate Alias + * @param string|array $data * @return array + * @throws ResultException */ - final protected function sperateAlias(string|array $data): array + final protected function separateAlias(string|array $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 " . + throw new ResultException("If you specify Table as array then it should look " . "like this [TABLE_NAME, ALIAS]", 1); } $alias = array_pop($data); @@ -211,15 +228,16 @@ final protected function sperateAlias(string|array $data): array } /** - * Propegate where data structure - * @param string|AttrInterface $key + * Propagate where data structure + * @param string|AttrInterface $key * @param string|int|float|AttrInterface $val * @param array|null &$data static value + * @throws DBValidationException */ final protected function setWhereData(string|AttrInterface $key, string|int|float|AttrInterface $val, ?array &$data): void { if (is_null($data)) { - $data = array(); + $data = []; } $key = (string)$this->prep($key, false); $val = $this->prep($val); @@ -228,12 +246,12 @@ 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][$key][] = [ "not" => $this->whereNot, "operator" => $this->compare, "value" => $val ]; + $this->whereNot = null; $this->whereProtocol[$key][] = $val; $this->resetWhere(); @@ -251,27 +269,26 @@ final protected function whereArrToStr(array $array): string foreach ($array as $key => $arr) { foreach ($arr as $col => $a) { if (is_array($a)) { - foreach ($a as $int => $row) { + foreach ($a as $row) { if ($count > 0) { - $out .= "{$key} "; + $out .= "$key "; } if ($row['not'] === true) { $out .= "NOT "; } - $out .= "{$col} {$row['operator']} {$row['value']} "; + $out .= "$col {$row['operator']} {$row['value']} "; $count++; } - + } else { - $out .= "{$key} {$a} "; + $out .= ($count) > 0 ? "$key $a " : $a; $count++; } } } - return $out; } - + /** * Get the Main FK data protocol * @return array @@ -279,7 +296,7 @@ final protected function whereArrToStr(array $array): string final protected function getMainFKData(): array { if (is_null($this->fkData)) { - $this->fkData = array(); + $this->fkData = []; foreach ($this->mig->getMig()->getData() as $col => $row) { if (isset($row['fk'])) { foreach ($row['fk'] as $a) { @@ -315,7 +332,7 @@ final protected function prep(mixed $val, bool $enclose = true): AttrInterface */ final protected function prepArr(array $arr, bool $enclose = true): array { - $new = array(); + $new = []; foreach ($arr as $pKey => $pVal) { $key = (string)$this->prep($pKey, false); $new[$key] = (string)$this->prep($pVal, $enclose); @@ -324,12 +341,12 @@ final protected function prepArr(array $arr, bool $enclose = true): array } /** - * Use vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually + * Use vsprintf to mysql prep/protect input in string. Prep string values needs to be enclosed 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 + final protected function sprint(string $str, array $arr = []): string { return vsprintf($str, $this->prepArr($arr, false)); } @@ -351,7 +368,8 @@ final protected function camelLoop(array $camelCaseArr, array $valArr, callable } /** - * Will extract camle case to array + * MOVE TO DTO ARR + * Will extract camelcase to array * @param string $value string value with possible camel cases * @return array */ @@ -369,7 +387,7 @@ final protected function extractCamelCase(string $value): array */ final protected function buildJoinFromMig(MigrateInterface $mig, string $type): array { - $joinArr = array(); + $joinArr = []; $prefix = $this->connInst()->getHandler()->getPrefix(); $main = $this->getMainFKData(); $data = $mig->getData(); @@ -403,6 +421,7 @@ final protected function buildJoinFromMig(MigrateInterface $mig, string $type): /** * Build on YB to col sql string part * @return string|null + * @throws ConnectException */ protected function getAllQueryTables(): ?string { @@ -420,7 +439,7 @@ protected function getAllQueryTables(): ?string * @param string|null $method * @param array $args * @return array|object|bool|string - * @throws DBQueryException + * @throws ResultException|ConnectException */ final protected function query(string|self $sql, ?string $method = null, array $args = []): array|object|bool|string { @@ -430,8 +449,8 @@ final protected function query(string|self $sql, ?string $method = null, array $ if (method_exists($query, $method)) { return call_user_func_array([$query, $method], $args); } - throw new DBQueryException("Method \"$method\" does not exists!", 1); + throw new ResultException("Method \"$method\" does not exists!", 1); } return $query; } -} \ No newline at end of file +} diff --git a/AbstractMigrate.php b/AbstractMigrate.php index d98ed0f..7b414c2 100755 --- a/AbstractMigrate.php +++ b/AbstractMigrate.php @@ -1,4 +1,5 @@ handler = $handler; } /** - * Prevent cloning the instance + * This will prevent cloning the instance * @return void */ - private function __clone() { + private function __clone(): void + { + } + + /** + * Access the database main class + * @param string $method + * @param array $arguments + * @return object|false + * @throws ConnectException + */ + public function __call(string $method, array $arguments): object|false + { + if(is_null($this->connection)) { + throw new ConnectException("The connection has not been initialized yet."); + } + return call_user_func_array([$this->connection, $method], $arguments); + } + + /** + * 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); + if(!self::hasInstance($key)) { + throw new ConnectException("Connection Error: No active connection or connection instance found."); + } + self::$current = $key; + return self::$inst[$key]; } /** @@ -42,17 +83,17 @@ private function __clone() { public static function __callStatic(string $name, array $arguments) { $inst = new DB(); - $inst->setConnKey(self::$current); + $inst->setConnKey(static::$current); return $inst::$name(...$arguments); } /** * Set connection handler - * @param $handler + * @param HandlerInterface $handler * @param string|null $key * @return self */ - public static function setHandler($handler, ?string $key = null): self + public static function setHandler(HandlerInterface $handler, ?string $key = null): self { $key = self::getKey($key); if(self::hasInstance($key)) { @@ -81,22 +122,6 @@ public static function removeHandler(string $key): void } } - /** - * 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); - if(!self::hasInstance($key)) { - throw new ConnectException("Connection Error: No active connection or connection instance found."); - } - self::$current = $key; - return self::$inst[$key]; - } - /** * Check if default instance or secondary instances exist for key * @param string|null $key @@ -109,39 +134,44 @@ public static function hasInstance(?string $key = null): bool } /** - * Get the possible connection key - * @param string|null $key - * @return string + * Connect to database + * The ConnectInterface instance will be null before execute + * @return void + * @throws ConnectException */ - private static function getKey(?string $key = null): string + public function execute(): void { - $key = (is_null($key)) ? "default" : $key; - return $key; + try { + $this->connection = $this->handler->execute(); + } catch(Exception $e) { + throw new ConnectException($e->getMessage(), $e->getCode(), $e); + } } /** - * Access the connection handler - * @return mixed + * Get current DB connection + * DEPRECATED: Use connection instead! */ - function getHandler() { - return $this->handler; + public function DB(): ConnectInterface + { + return $this->connection(); } /** - * Get database type - * @return string + * Get current DB connection */ - public function getType(): string + public function connection(): ConnectInterface { - return $this->handler->getType(); + return $this->connection; } + /** - * Get current table prefix - * @return string + * Access the connection handler + * @return HandlerInterface */ - public function getPrefix(): string + public function getHandler(): HandlerInterface { - return $this->handler->getPrefix(); + return $this->handler; } /** @@ -154,103 +184,141 @@ public function hasConnection(): bool } /** - * Connect to database - * @return void + * Protect/prep database values from injections + * @param string $value + * @return string */ - public function execute(): void + public function prep(string $value): string { - $this->db = $this->handler->execute(); + return $this->handler->prep($value); } /** - * Get current DB connection + * Query sql string + * @param string $query + * @param int $result_mode + * @return object|array|bool + * @throws ResultException */ - public function DB(): mixed + public function query(string $query, int $result_mode = 0): object|array|bool { - return $this->db; + try { + return $this->connection->query($query); + } catch (Exception $e) { + throw new ResultException($e->getMessage(), $e->getCode(), $e); + } } /** - * Query sql string - * @param string $sql - * @return object|array|bool + * Begin transaction + * @return bool */ - public function query(string $sql): object|array|bool + public function begin_transaction(): bool { - return $this->db->query($sql); + return $this->connection->begin_transaction(); } /** - * Protect/prep database values from injections - * @param string $value - * @return string + * Commit transaction + * @return bool */ - public function prep(string $value): string + public function commit(): bool { - return $this->handler->prep($value); + return $this->connection->commit(); } /** - * Select a new database - * @param string $databaseName - * @param string|null $prefix Expected table prefix (NOT database prefix) - * @return void + * Rollback transaction + * @return bool */ - /* - public static function selectDB(string $databaseName, ?string $prefix = null): void + public function rollback(): bool { - mysqli_select_db(static::$selectedDB, $databaseName); - if (!is_null($prefix)) { - static::setPrefix($prefix); - } + return $this->connection->rollback(); } + + /** + * Returns the value generated for an AI column by the last query + * @param string|null $column Is only used with PostgreSQL! + * @return int + */ + public function insert_id(?string $column = null): int + { + return $this->connection->insert_id($column); + } + + /** + * Close connection + * @return bool */ + public function close(): true + { + return $this->connection->close(); + } /** - * Execute multiple quries at once (e.g. from a sql file) - * @param string $sql - * @param object|null &$mysqli - * @return array + * Start Transaction will return instance of ConnectInterface instead of bool + * @return ConnectInterface + * @throws ConnectException */ - public function multiQuery(string $sql, object &$mysqli = null): array + public function transaction(): ConnectInterface { - return $this->handler->multiQuery($sql, $mysqli); + if(!$this->begin_transaction()) { + $errorMsg = "Couldn't start transaction!"; + if(!empty($this->connection->error)) { + $errorMsg = "The transaction error: " . $this->connection->error; + } + throw new ConnectException($errorMsg); + } + return $this->connection; } /** - * Start Transaction - * @return mysqli + * Get the possible connection key + * @param string|null $key + * @return string */ - public function transaction(): mixed + private static function getKey(?string $key = null): string { - return $this->handler->transaction(); + return (is_null($key)) ? "default" : $key; } /** - * Profile mysql speed + * MOVE TO HANDLERS + * This method will be CHANGED soon + * @param string|null $key + * @return self + * @throws ConnectException|ResultException */ - public static function startProfile(): void + public static function startProfile(?string $key = null): self { - Connect::query("set profiling=1"); + $inst = self::getInstance($key); + $inst->query("set profiling=1"); + return $inst; } /** + * MOVE TO HANDLERS + * This method will be CHANGED soon * Close profile and print results + * Expects startProfile + * @throws ResultException */ - public static function endProfile($html = true): string|array + public function endProfile($html = true): string|array { $totalDur = 0; - $result = Connect::query("show profiles"); + $result = $this->query("show profiles"); $output = ""; if ($html) { $output .= "

"; } - if (is_object($result)) while ($row = $result->fetch_object()) { - $dur = round($row->Duration, 4) * 1000; - $totalDur += $dur; - $output .= $row->Query_ID . ' - ' . $dur . ' ms - ' . $row->Query . "
\n"; + if (is_object($result)) { + while ($row = $result->fetch_object()) { + $dur = round($row->Duration, 4) * 1000; + $totalDur += $dur; + $output .= $row->Query_ID . ' - ' . $dur . ' ms - ' . $row->Query . "
\n"; + } } $total = round($totalDur, 4); @@ -259,55 +327,7 @@ public static function endProfile($html = true): string|array $output .= "

"; return $output; } else { - return array("row" => $output, "total" => $total); - } - } - - /** - * Create Mysql variable - * @param string $key Variable key - * @param string $value Variable value - */ - public static function setVariable(string $key, string $value): AttrInterface - { - $escapedVarName = Attr::value("@{$key}")->enclose(false)->encode(false); - $escapedValue = (($value instanceof AttrInterface) ? $value : Attr::value($value)); - - self::$mysqlVars[$key] = clone $escapedValue; - Connect::query("SET {$escapedVarName} = {$escapedValue}"); - return $escapedVarName; - } - - /** - * Get Mysql variable - * @param string $key Variable key - */ - public static function getVariable(string $key): AttrInterface - { - if (!self::hasVariable($key)) { - throw new ConnectException("DB MySQL variable is not set.", 1); - } - return Attr::value("@{$key}")->enclose(false)->encode(false); - } - - /** - * Get Mysql variable - * @param string $key Variable key - */ - public static function getVariableValue(string $key): string - { - if (!self::hasVariable($key)) { - throw new ConnectException("DB MySQL variable is not set.", 1); + return ["row" => $output, "total" => $total]; } - return self::$mysqlVars[$key]->enclose(false)->encode(false); - } - - /** - * Has Mysql variable - * @param string $key Variable key - */ - public static function hasVariable(string $key): bool - { - return (isset(self::$mysqlVars[$key])); } } diff --git a/ConnectTest.php b/ConnectTest.php new file mode 100755 index 0000000..38f2d7f --- /dev/null +++ b/ConnectTest.php @@ -0,0 +1,41 @@ +handler = $handler; + + if(is_null(self::$inst)) { + self::$inst = $this; + } + } + + public static function getConnection(): self + { + return self::$inst; + } + + public static function __callStatic(string $name, array $arguments): mixed + { + $inst = DBTest::{$name}(...$arguments); + $inst->setConnection(self::$inst); + } + + +} diff --git a/Create.php b/Create.php index b805496..e013cf4 100755 --- a/Create.php +++ b/Create.php @@ -67,13 +67,14 @@ namespace MaplePHP\Query; +use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Exceptions\QueryCreateException; class Create { private $sql; private $add; - private $addArr = array(); + private $addArr = []; private $prefix; private $type; private $args; @@ -87,15 +88,15 @@ class Create private $tbKeys; private $tbKeysType; //private $columnData; - private $keys = array(); - private $ai = array(); - private $fk = array(); - private $fkList = array(); - private $colData = array(); - private $rename = array(); - private $hasRename = array(); - private $renameTable = array(); - private $primaryKeys = array(); + private $keys = []; + private $ai = []; + private $fk = []; + private $fkList = []; + private $colData = []; + private $rename = []; + private $hasRename = []; + private $renameTable = []; + private $primaryKeys = []; private $dropPrimaryKeys = false; private $build; @@ -248,7 +249,7 @@ public function generated() { if (isset($this->args['generated'])) { $value = explode(",", $this->args['generated']['columns']); - $colArr = array(); + $colArr = []; if (isset($this->args['generated']['json_columns'])) { foreach ($value as $col) { preg_match('#\{{(.*?)\}}#', $col, $match); @@ -292,8 +293,8 @@ public function generated() */ private function adding() { - $arr = array(); - $methodArr = array("type", "generated", "attributes", "collation", "null", "default"); + $arr = []; + $methodArr = ["type", "generated", "attributes", "collation", "null", "default"]; foreach ($methodArr as $method) { if ($val = $this->{$method}()) { $arr[] = $val; @@ -536,17 +537,18 @@ public function build() /** * Execute * @return array errors. + * @throws ConnectException */ public function execute() { $sql = $this->build(); - $error = Connect::getInstance()->multiQuery($sql, $mysqli); + $error = Connect::getInstance()->getHandler()->multiQuery($sql, $mysqli); return $error; } public function mysqlCleanArr(array $arr) { - $new = array(); + $new = []; foreach ($arr as $a) { $new[] = Connect::getInstance()->prep($a); } @@ -560,7 +562,7 @@ public function mysqlCleanArr(array $arr) private function tbKeys(): array { if (is_null($this->tbKeys)) { - $this->tbKeysType = $this->tbKeys = array(); + $this->tbKeysType = $this->tbKeys = []; if ($this->tableExists($this->table)) { $result = Connect::getInstance()->query("SHOW INDEXES FROM {$this->table}"); if (is_object($result) && $result->num_rows > 0) { @@ -816,7 +818,7 @@ private function buildKeys(): string $prepareDrop = $this->tbKeysType(); if (count($this->keys) > 0) { - $sqlKeyArr = array(); + $sqlKeyArr = []; foreach ($this->keys as $col => $key) { $col = Connect::getInstance()->prep($col); $key = strtoupper(Connect::getInstance()->prep($key)); @@ -917,7 +919,7 @@ public function fkExists(string $table, string $col) "INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = '{$dbName}' AND " . "TABLE_NAME = '{$table}' AND COLUMN_NAME = '{$col}'"); - $arr = array(); + $arr = []; if (is_object($result) && $result->num_rows > 0) { while ($row = $result->fetch_object()) { $arr[$row->CONSTRAINT_NAME] = $row; @@ -954,4 +956,4 @@ public function columnExists(string $table, string $col) } return false; } -} \ No newline at end of file +} diff --git a/DB.php b/DB.php index 97714ef..fdf0d89 100755 --- a/DB.php +++ b/DB.php @@ -1,14 +1,16 @@ method = $method; - $inst->setConnKey(Connect::$current); - $prefix = Connect::getInstance(Connect::$current)->getHandler()->getPrefix(); + //$inst->setConnKey(Connect::$current); + $prefix = $inst->connInst()->getHandler()->getPrefix(); switch ($inst->method) { case 'select': @@ -83,6 +84,7 @@ public static function __callStatic(string $method, array $args) } else { $inst = new self(); + //$inst->setConnKey(Connect::$current); } return $inst; @@ -93,8 +95,8 @@ public static function __callStatic(string $method, array $args) * @param string $method * @param array $args * @return array|bool|DB|object - * @throws DBQueryException - * @throws DBValidationException|ConnectException + * @throws ResultException + * @throws DBValidationException|ConnectException|ResultException|Exceptions\DBQueryException */ public function __call(string $method, array $args) { @@ -104,7 +106,7 @@ public function __call(string $method, array $args) case "pluck": // Columns?? $args = ($args[0] ?? ""); if (str_contains($args, ",")) { - throw new DBQueryException("Your only allowed to pluck one database column!"); + throw new ResultException("Your only allowed to pluck one database column!"); } $pluck = explode(".", $args); @@ -141,7 +143,7 @@ public function __call(string $method, array $args) * It is better to use (DB::select, DB::insert, DB::update, DB::delete) * @param string|array|MigrateInterface $data * @return self new instance - * @throws DBQueryException + * @throws ResultException */ public static function table(string|array|MigrateInterface $data): self { @@ -152,7 +154,7 @@ public static function table(string|array|MigrateInterface $data): self } $inst = new self(); - $data = $inst->sperateAlias($data); + $data = $inst->separateAlias($data); $inst->alias = $data['alias']; $inst->table = $inst->getAttr($data['table'])->enclose(false); $inst->mig = $mig; @@ -190,7 +192,7 @@ 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 + * @throws DBValidationException|ConnectException */ protected function select(): self { @@ -209,6 +211,7 @@ protected function select(): self * Select view * @return self * @throws DBValidationException + * @throws ConnectException */ protected function selectView(): self { @@ -218,6 +221,7 @@ protected function selectView(): self /** * Build INSERT sql code (The method will be auto called in method build) * @return self + * @throws ConnectException */ protected function insert(): self { @@ -229,6 +233,7 @@ protected function insert(): self /** * Build UPDATE sql code (The method will be auto called in method build) * @return self + * @throws ConnectException */ protected function update(): self { @@ -244,6 +249,7 @@ protected function update(): self /** * Build DELETE sql code (The method will be auto called in method build) * @return self + * @throws ConnectException */ protected function delete(): self { @@ -259,48 +265,6 @@ protected function delete(): self return $this; } - /** - * Build CREATE VIEW sql code (The method will be auto called in method build) - * @return self - */ - protected function createView(): self - { - //$this->select(); - $this->sql = "CREATE VIEW " . $this->viewName . " AS $this->sql"; - return $this; - } - - /** - * Build CREATE OR REPLACE VIEW sql code (The method will be auto called in method build) - * @return self - */ - protected function replaceView(): self - { - //$this->select(); - $this->sql = "CREATE OR REPLACE VIEW " . $this->viewName . " AS $this->sql"; - return $this; - } - - /** - * Build DROP VIEW sql code (The method will be auto called in method build) - * @return self - */ - protected function dropView(): self - { - $this->sql = "DROP VIEW " . $this->viewName; - return $this; - } - - /** - * Build DROP VIEW sql code (The method will be auto called in method build) - * @return self - */ - protected function showView(): self - { - $this->sql = "SHOW CREATE VIEW " . $this->viewName; - return $this; - } - /** * Select protected mysql columns * @param string $columns @@ -385,19 +349,19 @@ public function whereRaw(string $sql, ...$arr): self /** * Create protected MySQL WHERE input * Supports dynamic method name calls like: whereIdStatus(1, 0) - * @param string|AttrInterface $key Mysql column - * @param string|int|float|AttrInterface $val Equals to value + * @param string|AttrInterface $column Mysql column + * @param string|int|float|AttrInterface $value 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 + public function where(string|AttrInterface $column, string|int|float|AttrInterface $value, ?string $operator = null): self { // Whitelist operator if (!is_null($operator)) { $this->compare = $this->operator($operator); } - $this->setWhereData($key, $val, $this->where); + $this->setWhereData($column, $value, $this->where); return $this; } @@ -419,18 +383,18 @@ public function whereBind(callable $call): self /** * Create protected MySQL HAVING input - * @param string|AttrInterface $key Mysql column - * @param string|int|float|AttrInterface $val Equals to value + * @param string|AttrInterface $column Mysql column + * @param string|int|float|AttrInterface $value 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 + public function having(string|AttrInterface $column, string|int|float|AttrInterface $value, ?string $operator = null): self { if (!is_null($operator)) { $this->compare = $this->operator($operator); } - $this->setWhereData($key, $val, $this->having); + $this->setWhereData($column, $value, $this->having); return $this; } @@ -479,20 +443,20 @@ public function offset(int $offset): self /** * Set Mysql ORDER - * @param string|AttrInterface $col Mysql Column + * @param string|AttrInterface $column 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 + public function order(string|AttrInterface $column, string $sort = "ASC"): self { - $col = $this->prep($col, false); + $column = $this->prep($column, false); - if (!is_null($this->mig) && !$this->mig->columns([(string)$col])) { + if (!is_null($this->mig) && !$this->mig->columns([(string)$column])) { throw new DBValidationException($this->mig->getMessage(), 1); } $sort = $this->orderSort($sort); // Whitelist - $this->order[] = "$col $sort"; + $this->order[] = "$column $sort"; return $this; } @@ -546,29 +510,29 @@ public function returning(string $column): self * @param string $type Type of join * @return self * @throws ConnectException - * @throws DBQueryException + * @throws ResultException * @throws DBValidationException */ public function join( string|array|MigrateInterface $table, string|array $where = null, - array $sprint = array(), + array $sprint = [], 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 argument 2 (where) value!", 1); + throw new ResultException("You need to specify the argument 2 (where) value!", 1); } $prefix = $this->connInst()->getHandler()->getPrefix(); - $arr = $this->sperateAlias($table); + $arr = $this->separateAlias($table); $table = (string)$this->prep($arr['table'], false); $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " $table"; if (is_array($where)) { - $data = array(); + $data = []; foreach ($where as $key => $val) { if (is_array($val)) { foreach ($val as $grpKey => $grpVal) { @@ -606,7 +570,8 @@ public function distinct(): self } /** - * Explain the mysql query. Will tell you how you can make improvements + * Explain the query. Will tell you how you can make improvements + * All database handlers is supported e.g. mysql, postgresql, sqlite... * @return self */ public function explain(): self @@ -617,6 +582,7 @@ public function explain(): self /** * Disable mysql query cache + * All database handlers is supported e.g. mysql, postgresql, sqlite... * @return self */ public function noCache(): self @@ -627,6 +593,7 @@ public function noCache(): self /** * DEPRECATE: Calculate rows in query + * All database handlers is supported e.g. mysql, postgresql, sqlite... * @return self */ public function calcRows(): self @@ -676,7 +643,7 @@ public function onDupKey($key = null, ?string $value = null): self // Same as onDupKey public function onDuplicateKey($key = null, ?string $value = null): self { - $this->dupSet = array(); + $this->dupSet = []; if (!is_null($key)) { if (is_array($key)) { $this->dupSet = $this->prepArr($key); @@ -700,14 +667,14 @@ public function union(DBInterface $inst, bool $allowDuplicate = false): self return $this->unionRaw($inst->sql(), $allowDuplicate); } - /** - * Union raw result, create union with raw SQL code - * @param string $sql - * @param bool $allowDuplicate UNION by default selects only distinct values. - * Use UNION ALL to also select duplicate values! - * @mixin AbstractDB - * @return self - */ + /** + * Union raw result, create union with raw SQL code + * @param string $sql + * @param bool $allowDuplicate UNION by default selects only distinct values. + * Use UNION ALL to also select duplicate values! + * @mixin AbstractDB + * @return self + */ public function unionRaw(string $sql, bool $allowDuplicate = false): self { $this->order = null; @@ -742,7 +709,7 @@ private function buildUpdateSet(?array $arr = null): string if (is_null($arr)) { $arr = $this->set; } - $new = array(); + $new = []; foreach ($arr as $key => $val) { $new[] = "$key = $val"; } @@ -753,6 +720,7 @@ private function buildUpdateSet(?array $arr = null): string * Will build a returning value that can be fetched with insert id * This is a PostgreSQL specific function. * @return string + * @throws ConnectException */ private function buildReturning(): string { @@ -792,6 +760,7 @@ private function buildWhere(string $prefix, ?array $where): string $out .= (($index > 0) ? " $firstAnd" : "") . " ("; $out .= $this->whereArrToStr($array); $out .= ")"; + $index++; } } @@ -822,15 +791,18 @@ private function buildLimit(): string /** * Used to call method that builds SQL queries - * @throws DBQueryException|DBValidationException + * @throws ResultException|DBValidationException|ConnectException */ final protected function build(): void { + if (!is_null($this->method) && method_exists($this, $this->method)) { + + $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 1 \"$inst->method\" expect to return a sql " . + throw new ResultException("The Method 1 \"$inst->method\" expect to return a sql " . "building method (like return @select() or @insert()).", 1); } } else { @@ -841,7 +813,7 @@ final protected function build(): void /** * Generate SQL string of current instance/query * @return string - * @throws DBQueryException|DBValidationException + * @throws ConnectException|DBValidationException|DBQueryException|ResultException */ public function sql(): string { @@ -851,20 +823,62 @@ public function sql(): string /** * Get insert AI ID from prev inserted result + * @param string|null $column * @return int|string - * @throws ConnectException|DBQueryException + * @throws ConnectException */ - public function insertID(): int|string + public function insertId(?string $column = null): int|string { - 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(); + $column = !is_null($column) ? $column : $this->returning; + if(!is_null($column)) { + return $this->connInst()->DB()->insert_id($column); } - return $this->connInst()->DB()->insert_id; + return $this->connInst()->DB()->insert_id(); + } + + /** + * DEPRECATED?? + */ + + /** + * Build CREATE VIEW sql code (The method will be auto called in method build) + * @return self + */ + protected function createView(): self + { + //$this->select(); + $this->sql = "CREATE VIEW " . $this->viewName . " AS $this->sql"; + return $this; + } + + /** + * Build CREATE OR REPLACE VIEW sql code (The method will be auto called in method build) + * @return self + */ + protected function replaceView(): self + { + //$this->select(); + $this->sql = "CREATE OR REPLACE VIEW " . $this->viewName . " AS $this->sql"; + return $this; + } + + /** + * Build DROP VIEW sql code (The method will be auto called in method build) + * @return self + */ + protected function dropView(): self + { + $this->sql = "DROP VIEW " . $this->viewName; + return $this; + } + + /** + * Build DROP VIEW sql code (The method will be auto called in method build) + * @return self + */ + protected function showView(): self + { + $this->sql = "SHOW CREATE VIEW " . $this->viewName; + return $this; } -} \ No newline at end of file +} diff --git a/DBTest.php b/DBTest.php new file mode 100755 index 0000000..131103c --- /dev/null +++ b/DBTest.php @@ -0,0 +1,675 @@ +handler = $handler; + $this->connection = $handler->execute(); + $this->prefix = $handler->getPrefix(); + $this->attr = new Attr($this->connection); + } + + public function getConnection() + { + return $this->connection; + } + + public function __toString(): string + { + return $this->sql(); + } + + /** + * Used to make methods into dynamic shortcuts + * @param string $method + * @param array $args + * @return array|bool|object|string + * @throws ConnectException + * @throws DBValidationException + * @throws ResultException + */ + public function __call(string $method, array $args): array|bool|object|string + { + $camelCaseArr = Helpers::extractCamelCase($method); + $shift = array_shift($camelCaseArr); + + $inst = clone $this; + switch ($shift) { + case "pluck": // Columns?? + $args = ($args[0] ?? ""); + if (str_contains($args, ",")) { + throw new ResultException("Your only allowed to pluck one database column!"); + } + + $pluck = explode(".", $args); + $inst->pluck = trim(end($pluck)); + $inst = $inst->columns($args); + break; + case "where": + case "having": + Helpers::camelLoop($camelCaseArr, $args, function ($col, $val) use ($shift, &$inst) { + $inst = $inst->{$shift}($col, $val); + }); + break; + case "order": + if ($camelCaseArr[0] === "By") { + array_shift($camelCaseArr); + } + $ace = end($camelCaseArr); + foreach ($args as $val) { + $inst = $inst->order($val, $ace); + } + break; + case "join": + $inst = $inst->join($args[0], ($args[1] ?? null), ($args[2] ?? []), $camelCaseArr[0]); + break; + default: + return $inst->query($inst, $method, $args); + } + return $inst; + } + + // Magic method to dynamically access protected properties + public function __get($property) + { + if (property_exists($this, $property)) { + return $this->{$property}; + } + throw new InvalidArgumentException("Property '$property' does not exist"); + } + + /** + * @param string|array|MigrateInterface $table + * @return DBTest + */ + public function table(string|array|MigrateInterface $table): self + { + $inst = clone $this; + + /* + if ($table instanceof MigrateInterface) { + $inst->migration = new WhitelistMigration($table); + $table = $inst->migration->getTable(); + } + */ + $tableRow = Helpers::separateAlias($table); + $table = $inst->prefix . $tableRow['table']; + + + $inst->table = $inst->attr($table, Attr::COLUMN_TYPE); + $inst->alias = $this->attr($tableRow['alias'] ?? $tableRow['table'], Attr::COLUMN_TYPE); + + + return $inst; + } + + /** + * Easy way to create a attr/data type for the query string + * + * @param mixed $value + * @param int $type + * @return array|AttrInterface + */ + public function attr(mixed $value, int $type): array|AttrInterface + { + if(is_callable($value)) { + $value = $value($this->attr->withValue($value)->type($type)); + } + if(is_array($value)) { + return array_map(function ($val) use ($type) { + if($val instanceof AttrInterface) { + return $val; + } + return $this->attr->withValue($val)->type($type); + }, $value); + } + if($value instanceof AttrInterface) { + return $value; + } + return $this->attr->withValue($value)->type($type); + } + + /** + * When SQL query has been triggered then the QueryBuilder should exist + * @return QueryBuilderInterface + */ + public function getQueryBuilder(): QueryBuilderInterface + { + if(is_null($this->builder)) { + $this->sql(); + //throw new BadMethodCallException("The query builder can only be called after query has been built."); + } + return $this->builder; + } + + /** + * Select protected mysql columns + * + * @param string|array|AttrInterface ...$columns + * @return self + */ + public function columns(string|array|AttrInterface ...$columns): self + { + $inst = clone $this; + foreach ($columns as $key => $column) { + $inst->columns[$key]['alias'] = null; + if(is_array($column)) { + $alias = reset($column); + $column = key($column); + $inst->columns[$key]['alias'] = $this->attr($alias, Attr::COLUMN_TYPE); + } + $inst->columns[$key]['column'] = $this->attr($column, Attr::COLUMN_TYPE); + } + return $inst; + } + + // JUST A IF STATEMENT + // whenNot??? + public function when(bool $bool, callable $func): self + { + $inst = clone $this; + return $inst; + } + + // FIXA - SQL STATEMENT EXISTS + // existNot??? + public function exist(callable $func): self + { + $inst = clone $this; + return $inst; + } + + /** + * Change where compare operator from default "=". + * Will change back to default after where method is triggered + * @param string $operator once of (">", ">=", "<", "<>", "!=", "<=", "<=>") + * @return self + */ + public function compare(string $operator): self + { + $inst = clone $this; + $inst->compare = Helpers::operator($operator); + return $inst; + } + + /** + * Chaining where with mysql "AND" or with "OR" + * @return self + */ + public function and(): self + { + $inst = clone $this; + $inst->whereAnd = "AND"; + return $inst; + } + + /** + * Chaining where with mysql "AND" or with "OR" + * @return self + */ + public function or(): self + { + $inst = clone $this; + $inst->whereAnd = "OR"; + return $inst; + } + + /** + * Chaining with where "NOT" ??? + * @return self + */ + public function not(): self + { + $inst = clone $this; + $inst->whereNot = true; + return $inst; + } + + + /** + * Create protected MySQL WHERE input + * Supports dynamic method name calls like: whereIdStatus(1, 0) + * @param string|AttrInterface $column Mysql column + * @param string|int|float|AttrInterface $value Equals to value + * @param string|null $operator Change comparison operator from default "=". + * @return self + */ + public function where(string|AttrInterface $column, string|int|float|AttrInterface $value, ?string $operator = null): self + { + $inst = clone $this; + if (!is_null($operator)) { + $inst->compare = Helpers::operator($operator); + } + $inst->setWhereData($value, $column, $inst->where); + $inst->set[] = (string)$value; + return $inst; + } + + /** + * Create protected MySQL HAVING input + * @param string|AttrInterface $column Mysql column + * @param string|int|float|AttrInterface $value Equals to value + * @param string|null $operator Change comparison operator from default "=". + * @return self + */ + public function having(string|AttrInterface $column, string|int|float|AttrInterface $value, ?string $operator = null): self + { + $inst = clone $this; + if (!is_null($operator)) { + $inst->compare = Helpers::operator($operator); + } + $this->setWhereData($value, $column, $inst->having); + return $inst; + } + + /** + * Set Mysql ORDER + * @param string|AttrInterface $column Mysql Column + * @param string $sort Mysql sort type. Only "ASC" OR "DESC" is allowed, anything else will become "ASC". + * @return self + */ + public function order(string|AttrInterface $column, string $sort = "ASC"): self + { + // PREP AT BUILD + //$col = $this->prep($col, false); + /* + if (!is_null($this->migration) && !$this->migration->columns([(string)$col])) { + throw new DBValidationException($this->migration->getMessage(), 1); + } + */ + $inst = clone $this; + $inst->order[] = [ + "column" => $this->attr($column, Attr::COLUMN_TYPE), + "sort" => Helpers::orderSort($sort) + ]; + return $inst; + } + + /** + * Add a limit and maybe an offset + * @param int|AttrInterface $limit + * @param int|AttrInterface|null $offset + * @return $this + */ + public function limit(int|AttrInterface $limit, null|int|AttrInterface $offset = null): self + { + $inst = clone $this; + $inst->limit = $this->attr($limit, Attr::VALUE_TYPE_NUM); + if (!is_null($offset)) { + $inst->offset($offset); + } + return $inst; + } + + /** + * Add an offset (if limit is not set then it will automatically become "1"). + * @param int|AttrInterface $offset + * @return $this + */ + public function offset(int|AttrInterface $offset): self + { + $inst = clone $this; + $inst->offset = $this->attr($offset, Attr::VALUE_TYPE_NUM); + return $inst; + } + + + /** + * Add group + * @param array $columns + * @return self + */ + public function group(...$columns): self + { + /* + if (!is_null($this->migration) && !$this->migration->columns($columns)) { + throw new DBValidationException($this->migration->getMessage(), 1); + } + */ + $inst = clone $this; + $inst->group = $columns; + return $inst; + } + + /** + * Add make query a distinct call + * @return self + */ + public function distinct(): self + { + $inst = clone $this; + $inst->distinct = true; + return $inst; + } + + /** + * Postgre specific function + * @param string $column + * @return $this + */ + public function returning(string $column): self + { + $inst = clone $this; + $inst->returning = $column; + return $inst; + } + + /** + * Mysql JOIN query (Default: INNER) + * @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 + */ + public function join( + string|array|MigrateInterface $table, + string|array $where = null, + array $sprint = [], + string $type = "INNER" + ): self { + + $inst = clone $this; + if ($table instanceof MigrateInterface) { + die("FIX"); + + } else { + + /* + * if (is_null($where)) { + throw new ResultException("You need to specify the argument 2 (where) value!", 1); + } + */ + + // Try to move this to the start of the method + $tableInst = clone $inst; + $tableInst->alias = null; + $tableInst = $tableInst->table($table); + + $data = []; + if (is_array($where)) { + foreach ($where as $key => $val) { + if (is_array($val)) { + foreach ($val as $grpKey => $grpVal) { + $inst->setWhereData($this->attr($grpVal, Attr::COLUMN_TYPE), $grpKey, $data); + } + } else { + $inst->setWhereData($this->attr($val, Attr::COLUMN_TYPE), $key, $data); + } + } + } + $type = Helpers::joinTypes(strtoupper($type)); // Whitelist + + $inst->join[] = [ + "type" => $type, + "table" => $tableInst->table, + "alias" => $tableInst->alias, + "where" => $where, + "whereData" => $data, + "sprint" => $sprint + ]; + } + return $inst; + } + + /** + * Union result + * @param DBInterface $inst + * @param bool $allowDuplicate UNION by default selects only distinct values. + * Use UNION ALL to also select duplicate values! + * @mixin AbstractDB + * @return self + */ + public function union(DBInterface|string $dbInst, bool $allowDuplicate = false): self + { + $inst = clone $this; + + + if(!is_null($inst->order)) { + throw new \RuntimeException("You need to move your ORDER BY to the last UNION statement!"); + } + + if(!is_null($inst->limit)) { + throw new \RuntimeException("You need to move your ORDER BY to the last UNION statement!"); + } + + $inst->union[] = [ + 'inst' => $dbInst, + 'allowDuplicate' => $allowDuplicate + ]; + return $inst; + } + + public function prepare(): self + { + $inst = clone $this; + $this->prepare = true; + return $this; + } + + + public function sql(): string + { + $this->builder = new QueryBuilder($this); + $sql = $this->builder->sql(); + return $sql; + } + + /** + * Propagate where data structure + * @param string|AttrInterface $key + * @param string|int|float|AttrInterface $val + * @param array|null &$data static value + */ + final protected function setWhereData(string|int|float|AttrInterface $val, string|AttrInterface $key, ?array &$data): void + { + if (is_null($data)) { + $data = []; + } + /* + $key = (string)$this->prep($key, false); + $val = $this->prep($val); + if (!is_null($this->migration) && !$this->migration->where($key, $val)) { + throw new DBValidationException($this->migration->getMessage(), 1); + } + */ + + $data[$this->whereIndex][$this->whereAnd][$key][] = [ + "column" => $this->attr($key, Attr::COLUMN_TYPE), + "not" => $this->whereNot, + "operator" => $this->compare, + "value" => $this->attr($val, Attr::VALUE_TYPE) + ]; + + $this->resetWhere(); + } + + + /** + * Group mysql WHERE inputs + * @param callable $call Every method where placed inside callback will be grouped. + * @return self + */ + public function whereBind(callable $call): self + { + $inst = clone $this; + if (!is_null($inst->where)) { + $inst->whereIndex++; + } + $inst->resetWhere(); + $call($inst); + $inst->whereIndex++; + return $inst; + } + + + /** + * Will reset Where input + * No need to clone as this will return void + * @return void + */ + protected function resetWhere(): void + { + $this->whereNot = false; + $this->whereAnd = "AND"; + $this->compare = "="; + } + + + /** + * Query result + * @param string|self $sql + * @param string|null $method + * @param array $args + * @return array|object|bool|string + * @throws ResultException + */ + final public function query(string|self $sql, ?string $method = null, array $args = []): array|object|bool|string + { + $query = new Query($this->connection, $sql); + $query->setPluck($this->pluck); + if (!is_null($method)) { + if (method_exists($query, $method)) { + return call_user_func_array([$query, $method], $args); + } + throw new ResultException("Method \"$method\" does not exists!", 1); + } + return $query; + } + + /** + * Execute + * @return mixed + * @throws ResultException + */ + function execute() + { + if(is_null($this->result)) { + $this->result = $this->query($this->sql())->execute(); + } + return $this->result; + } + + + /** + MIGRATION BUILDERS + */ + + /** + * Build join data from Migrate data + * @param MigrateInterface $mig + * @param string $type Join type (INNER, LEFT, ...) + * @return array + * @throws ConnectException + */ + final protected function buildJoinFromMig(MigrateInterface $mig, string $type): array + { + $joinArr = []; + $prefix = $this->connInst()->getHandler()->getPrefix(); + $main = $this->getMainFKData(); + $data = $mig->getData(); + $this->migration->mergeData($data); + $migTable = $mig->getTable(); + + foreach ($data as $col => $row) { + 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']})"; + } + } + } 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)"; + } + } + } + } + } + return $joinArr; + } + + /** + * Get the Main FK data protocol + * @return array + */ + final protected function getMainFKData(): array + { + if (is_null($this->fkData)) { + $this->fkData = []; + 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; + } + +} diff --git a/Exceptions/DBQueryException.php b/Exceptions/ResultException.php similarity index 62% rename from Exceptions/DBQueryException.php rename to Exceptions/ResultException.php index 1ffc502..69e4d6f 100755 --- a/Exceptions/DBQueryException.php +++ b/Exceptions/ResultException.php @@ -5,10 +5,10 @@ use Exception; /** - * Class DBQueryException + * Class ResultException * * @package MaplePHP\Query\Exceptions */ -class DBQueryException extends Exception +class ResultException extends Exception { } diff --git a/Handlers/MySQL/MySQLConnect.php b/Handlers/MySQL/MySQLConnect.php new file mode 100644 index 0000000..5ab5632 --- /dev/null +++ b/Handlers/MySQL/MySQLConnect.php @@ -0,0 +1,94 @@ +getMessage(), $e->getCode(), $e); + } + } + + /** + * Access the database main class + * @param string $method + * @param array $arguments + * @return object|false + */ + public function __call(string $method, array $arguments): object|false + { + return call_user_func_array([$this, $method], $arguments); + } + + /** + * Performs a query on the database + * https://www.php.net/manual/en/mysqli.query.php + * @param string $query + * @param int $result_mode + * @return mysqli_result|bool + */ + public function query(string $query, int $result_mode = MYSQLI_STORE_RESULT): mysqli_result|bool + { + return parent::query($query, $result_mode); + } + + /** + * Performs a query on the database + * https://www.php.net/manual/en/mysqli.query.php + * @param string $query + * @return mysqli_result|bool + */ + public function prepare(string $query): mysqli_stmt|false + { + return parent::prepare($query); + } + + /** + * Returns the value generated for an AI column by the last query + * @param string|null $column Is only used with PostgreSQL! + * @return int + */ + public function insert_id(?string $column = null): int + { + return $this->insert_id; + } + + /** + * Close connection + * @return bool + */ + public function close(): true + { + return $this->close(); + } + + /** + * Prep value / SQL escape string + * @param string $value + * @return string + */ + public function prep(string $value): string + { + return $this->real_escape_string($value); + } +} diff --git a/Handlers/MySQLHandler.php b/Handlers/MySQLHandler.php index ee25f28..dc89217 100755 --- a/Handlers/MySQLHandler.php +++ b/Handlers/MySQLHandler.php @@ -1,10 +1,13 @@ connection = new mysqli($this->server, $this->user, $this->pass, $this->dbname, $this->port); + $this->connection = new MySQLConnect($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); } @@ -153,7 +156,7 @@ public function prep(string $value): string public function multiQuery(string $sql, object &$db = null): array { $count = 0; - $err = array(); + $err = []; $db = $this->connection; if (mysqli_multi_query($db, $sql)) { do { @@ -175,15 +178,4 @@ public function multiQuery(string $sql, object &$db = null): array } 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 index bc3fc4a..9e1489c 100755 --- a/Handlers/PostgreSQL/PostgreSQLConnect.php +++ b/Handlers/PostgreSQL/PostgreSQLConnect.php @@ -1,60 +1,106 @@ connection = pg_connect("host=$server port=$port dbname=$dbname user=$user password=$pass"); - if (!$this->connection) { - $this->error = pg_last_error(); + + try { + $this->connection = pg_connect("host=$server port=$port dbname=$dbname user=$user password=$pass"); + if (!is_null($this->connection)) { + $this->error = pg_last_error($this->connection); + } + } catch (Exception $e) { + throw new ConnectException('Failed to connect to PostgreSQL: ' . $e->getMessage(), $e->getCode(), $e); } + + $this->key = "postgre_query_" . self::$index; + self::$index++; + } + /** + * Get connection + * @return Connection + */ public function getConnection(): Connection { return $this->connection; } + /** + * Make a prepare statement + * @param string $query + * @return StmtInterface|false + */ + public function prepare(string $query): StmtInterface|false + { + $index = 1; + $query = preg_replace_callback('/\?/', function() use(&$index) { + return '$' . $index++; + }, $query); + + + if (pg_prepare($this->connection, $this->key, $query)) { + return new PostgreSQLStmt($this->connection, $this->key); + } + return false; + } + /** * Returns Connection of PgSql\Connection - * @param string $name + * @param string $method * @param array $arguments * @return Connection|false */ - public function __call(string $name, array $arguments): Connection|false + public function __call(string $method, array $arguments): Connection|false { - return call_user_func_array([$this->connection, $name], $arguments); + return call_user_func_array([$this->connection, $method], $arguments); } /** * Query sql - * @param $sql + * @param $query + * @param int $result_mode * @return PostgreSQLResult|bool */ - function query($sql): PostgreSQLResult|bool + public function query($query, int $result_mode = 0): PostgreSQLResult|bool { if($this->connection instanceof Connection) { $this->query = new PostgreSQLResult($this->connection); - if($query = $this->query->query($sql)) { + if($query = $this->query->query($query)) { return $query; } - $this->error = pg_result_error($this->connection); + $this->error = pg_result_error($this->query); } return false; } @@ -63,7 +109,7 @@ function query($sql): PostgreSQLResult|bool * Begin transaction * @return bool */ - function begin_transaction(): bool + public function begin_transaction(): bool { return (bool)$this->query("BEGIN"); } @@ -72,7 +118,7 @@ function begin_transaction(): bool * Commit transaction * @return bool */ - function commit(): bool + public function commit(): bool { return (bool)$this->query("COMMIT"); } @@ -81,7 +127,7 @@ function commit(): bool * Rollback transaction * @return bool */ - function rollback(): bool + public function rollback(): bool { return (bool)$this->query("ROLLBACK"); } @@ -89,18 +135,33 @@ function rollback(): bool /** * Get insert ID * @return mixed + * @throws ResultException */ - function insert_id(?string $column = null): int + public function insert_id(?string $column = null): int { + if(is_null($column)) { + throw new ResultException("PostgreSQL expects a column name for a return result."); + } return (int)pg_fetch_result($this->query, 0, $column); } /** * Close the connection - * @return void + * @return true */ - function close(): void + public function close(): true { pg_close($this->connection); + return true; + } + + /** + * Prep value / SQL escape string + * @param string $value + * @return string + */ + public function prep(string $value): string + { + return pg_escape_string($this->connection, $value); } } diff --git a/Handlers/PostgreSQL/PostgreSQLResult.php b/Handlers/PostgreSQL/PostgreSQLResult.php index ecfd402..cceb15b 100755 --- a/Handlers/PostgreSQL/PostgreSQLResult.php +++ b/Handlers/PostgreSQL/PostgreSQLResult.php @@ -1,4 +1,5 @@ connection = $connection; + $this->connection = $connection; + if(!is_null($query)) { + $this->query = $query; + $this->num_rows = pg_affected_rows($this->query); + } } /** diff --git a/Handlers/PostgreSQL/PostgreSQLStmt.php b/Handlers/PostgreSQL/PostgreSQLStmt.php new file mode 100755 index 0000000..19a944d --- /dev/null +++ b/Handlers/PostgreSQL/PostgreSQLStmt.php @@ -0,0 +1,78 @@ +connection = $connection; + $this->key = $key; + } + + /** + * Binds variables to a prepared statement as parameters + * https://www.php.net/manual/en/mysqli-stmt.bind-param.php + * @param string $types + * @param mixed $var + * @param mixed ...$vars + * @return bool + */ + public function bind_param(string $types, mixed &$var, mixed &...$vars): bool + { + $params = array_merge([$var], $vars); + $this->result = pg_execute($this->connection, $this->key, $params); + $this->success = ($this->result !== false); + return $this->success; + } + + /** + * Executes a prepared statement + * Not really needed in PostgreSQL but added as a placeholder + * https://www.php.net/manual/en/mysqli-stmt.execute.php + * @return bool + */ + public function execute(): bool + { + return $this->success; + } + + /** + * Gets a result set from a prepared statement as a ResultInterface object + * https://www.php.net/manual/en/mysqli-stmt.get-result.php + * @return ResultInterface + */ + public function get_result(): ResultInterface + { + return new PostgreSQLResult($this->connection, $this->result); + } + + /** + * Closes a prepared statement + * https://www.php.net/manual/en/mysqli-stmt.close.php + * @return true + */ + public function close(): true + { + pg_query($this->connection, "DEALLOCATE $this->key"); + return true; + } + +} diff --git a/Handlers/PostgreSQLHandler.php b/Handlers/PostgreSQLHandler.php index 0718f24..c8056a0 100755 --- a/Handlers/PostgreSQLHandler.php +++ b/Handlers/PostgreSQLHandler.php @@ -1,14 +1,15 @@ connection instanceof Connection); + return ($this->connection instanceof PostgreSQLConnect); } /** * Connect to database - * @return PostgreSQLConnect + * @return ConnectInterface * @throws ConnectException */ - public function execute(): PostgreSQLConnect + public function execute(): ConnectInterface { - $this->connection = new PostgreSQLConnect($this->server, $this->user, $this->pass, $this->dbname, $this->port); - if (!is_null($this->connection->error)) { + if (!empty($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); @@ -151,16 +151,6 @@ 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 @@ -170,7 +160,7 @@ public function transaction(): PostgreSQLConnect public function multiQuery(string $sql, object &$db = null): array { $count = 0; - $err = array(); + $err = []; $db = $this->connection->getConnection(); // Split the SQL string into individual queries $queries = explode(';', $sql); diff --git a/Handlers/SQLite/SQLiteConnect.php b/Handlers/SQLite/SQLiteConnect.php index 3c1f0f1..79eacb4 100644 --- a/Handlers/SQLite/SQLiteConnect.php +++ b/Handlers/SQLite/SQLiteConnect.php @@ -5,27 +5,27 @@ use Exception; use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\ConnectInterface; +use MaplePHP\Query\Interfaces\StmtInterface; use SQLite3; -use SQLite3Result; class SQLiteConnect implements ConnectInterface { - - public string|int $insert_id; - public string $error; + public string $error = ""; private SQLiteResult $query; private SQLite3 $connection; + /** + * @throws ConnectException + */ public function __construct(string $database) { try { $this->connection = new SQLite3($database); } catch (Exception $e) { - throw new ConnectException('Failed to connect to SQLite: ' . $e->getMessage(), 1); + throw new ConnectException('Failed to connect to SQLite: ' . $e->getMessage(), $e->getCode(), $e); } - return $this->connection; } /** @@ -42,17 +42,29 @@ public function __call(string $method, array $arguments): SQLite3|false /** * Performs a query on the database * @param string $query + * @param int $result_mode * @return object|false */ - public function query(string $query): SQLiteResult|false + public function query(string $query, int $result_mode = 0): SQLiteResult|false { $result = new SQLiteResult($this->connection); if($this->query = $result->query($query)) { return $this->query; } $this->error = $this->connection->lastErrorMsg(); + return false; + } - //$this->query = parent::query($query); + /** + * Make a prepare statement + * @param string $query + * @return StmtInterface|false + */ + public function prepare(string $query): StmtInterface|false + { + if ($stmt = $this->connection->prepare($query)) { + return new SQLiteStmt($this->connection, $stmt); + } return false; } @@ -60,7 +72,7 @@ public function query(string $query): SQLiteResult|false * Begin transaction * @return bool */ - function begin_transaction(): bool + public function begin_transaction(): bool { return (bool)$this->query("BEGIN TRANSACTION"); } @@ -69,7 +81,7 @@ function begin_transaction(): bool * Commit transaction * @return bool */ - function commit(): bool + public function commit(): bool { return (bool)$this->query("COMMIT"); } @@ -78,7 +90,7 @@ function commit(): bool * Rollback transaction * @return bool */ - function rollback(): bool + public function rollback(): bool { return (bool)$this->query("ROLLBACK"); } @@ -88,9 +100,28 @@ function rollback(): bool * @param string|null $column Is only used with PostgreSQL! * @return int */ - function insert_id(?string $column = null): int + public function insert_id(?string $column = null): int { return $this->connection->lastInsertRowID(); } -} \ No newline at end of file + /** + * Close connection + * @return bool + */ + public function close(): true + { + return true; + } + + /** + * Prep value / SQL escape string + * @param string $value + * @return string + */ + public function prep(string $value): string + { + return SQLite3::escapeString($value); + } + +} diff --git a/Handlers/SQLite/SQLiteResult.php b/Handlers/SQLite/SQLiteResult.php index 3eacd81..b165a74 100644 --- a/Handlers/SQLite/SQLiteResult.php +++ b/Handlers/SQLite/SQLiteResult.php @@ -1,28 +1,31 @@ connection = $connection; + $this->query = $query; + if($this->query !== false) { + $this->preFetchData(); + } } /** @@ -56,7 +59,6 @@ public function fetch_object(string $class = "stdClass", array $constructor_args $data = $this->bindToClass($data, $class, $constructor_args); } $this->endIndex(); - return $data; } @@ -137,7 +139,7 @@ protected function preFetchData(): void { $this->rowsObj = $this->rows = []; $this->num_rows = 0; - $obj = $arr = array(); + $obj = $arr = []; while ($row = $this->query->fetchArray(SQLITE3_ASSOC)) { $arr[] = $row; $obj[] = (object)$row; @@ -182,7 +184,7 @@ protected function endIndex(): void * @return object|string|null * @throws ReflectionException */ - protected function bindToClass(object|array $data, string $class, array $constructor_args = []): object|string|null + final protected function bindToClass(object|array $data, string $class, array $constructor_args = []): object|string|null { $reflection = new ReflectionClass($class); $object = $reflection->newInstanceArgs($constructor_args); diff --git a/Handlers/SQLite/SQLiteStmt.php b/Handlers/SQLite/SQLiteStmt.php new file mode 100755 index 0000000..4c7b578 --- /dev/null +++ b/Handlers/SQLite/SQLiteStmt.php @@ -0,0 +1,83 @@ +connection = $connection; + $this->stmt = $stmt; + } + + /** + * Binds variables to a prepared statement as parameters + * https://www.php.net/manual/en/mysqli-stmt.bind-param.php + * @param string $types + * @param mixed $var + * @param mixed ...$vars + * @return bool + */ + public function bind_param(string $types, mixed &$var, mixed &...$vars): bool + { + $params = array_merge([$var], $vars); + foreach($params as $key => $value) { + if(!$this->stmt->bindValue(($key + 1), $params[0], SQLITE3_TEXT)) { + return false; + } + } + return true; + } + + /** + * Executes a prepared statement + * Not really needed in PostgreSQL but added as a placeholder + * https://www.php.net/manual/en/mysqli-stmt.execute.php + * @return bool + */ + public function execute(): bool + { + $this->result = $this->stmt->execute(); + return ($this->result !== false); + } + + /** + * Gets a result set from a prepared statement as a ResultInterface object + * https://www.php.net/manual/en/mysqli-stmt.get-result.php + * @return ResultInterface + */ + public function get_result(): ResultInterface + { + return new SQLiteResult($this->connection, $this->result); + } + + /** + * Closes a prepared statement + * https://www.php.net/manual/en/mysqli-stmt.close.php + * @return true + */ + public function close(): true + { + $this->stmt->close(); + return true; + } + +} diff --git a/Handlers/SQLiteHandler.php b/Handlers/SQLiteHandler.php index 5cb96a0..de27401 100755 --- a/Handlers/SQLiteHandler.php +++ b/Handlers/SQLiteHandler.php @@ -1,4 +1,5 @@ connection)) { - $result = $this->connection->querySingle('PRAGMA quick_check'); - return $result === 'ok'; + $result = $this->connection->query('PRAGMA quick_check'); + $obj = $result->fetch_object(); + return ($obj->quick_check ?? "") === 'ok'; } return false; } @@ -72,13 +76,13 @@ public function hasConnection(): bool * @return SQLiteConnect * @throws ConnectException */ - public function execute(): SQLiteConnect + public function execute(): ConnectInterface { try { $this->connection = new SQLiteConnect($this->database); - + } catch (Exception $e) { - throw new ConnectException('Failed to connect to SQLite: ' . $e->getMessage(), 1); + throw new ConnectException('Failed to connect to SQLite: ' . $e->getMessage(), $e->getCode(), $e); } return $this->connection; } @@ -126,7 +130,6 @@ public function query(string $sql): bool|SQLiteResult */ public function close(): void { - $this->connection->close(); } /** @@ -136,7 +139,7 @@ public function close(): void */ public function prep(string $value): string { - return SQLiteConnect::escapeString($value); + return SQLite3::escapeString($value); } /** @@ -148,7 +151,7 @@ public function prep(string $value): string public function multiQuery(string $sql, object &$db = null): array { $count = 0; - $err = array(); + $err = []; $queries = explode(";", $sql); $db = $this->connection; foreach ($queries as $query) { @@ -164,14 +167,4 @@ public function multiQuery(string $sql, object &$db = null): array } return $err; } - - /** - * Start Transaction - * @return SQLiteConnect - */ - public function transaction(): SQLiteConnect - { - $this->connection->begin_transaction(); - return $this->connection; - } } diff --git a/Interfaces/AttrInterface.php b/Interfaces/AttrInterface.php index b261383..2e53320 100755 --- a/Interfaces/AttrInterface.php +++ b/Interfaces/AttrInterface.php @@ -16,13 +16,6 @@ public function __toString(); */ public function getRaw(): string|array; - /** - * Initiate the instance - * @param string $value - * @return self - */ - public static function value(array|string|int|float $value): self; - /** * Enable/disable MySQL prep * @param bool $prep diff --git a/Interfaces/ConnectInterface.php b/Interfaces/ConnectInterface.php index 3097eac..ddf27d8 100644 --- a/Interfaces/ConnectInterface.php +++ b/Interfaces/ConnectInterface.php @@ -4,7 +4,6 @@ interface ConnectInterface { - /** * Access the database main class * @param string $method @@ -16,29 +15,30 @@ public function __call(string $method, array $arguments): object|false; /** * Performs a query on the database * @param string $query - * @return object|false + * @param int $result_mode If database + * @return mixed */ - public function query(string $query): object|bool; + public function query(string $query, int $result_mode = 0): mixed; /** * Begin transaction * @return bool */ - function begin_transaction(): bool; + public function begin_transaction(): bool; /** * Commit transaction * @return bool */ - function commit(): bool; + public function commit(): bool; /** * Rollback transaction * @return bool */ - function rollback(): bool; + public function rollback(): bool; /** @@ -46,5 +46,18 @@ function rollback(): bool; * @param string|null $column Is only used with PostgreSQL! * @return int */ - function insert_id(?string $column = null): int; -} \ No newline at end of file + public function insert_id(?string $column = null): int; + + /** + * Close connection + * @return bool + */ + public function close(): true; + + /** + * Prep value / SQL escape string + * @param string $value + * @return string + */ + public function prep(string $value): string; +} diff --git a/Interfaces/DBInterface.php b/Interfaces/DBInterface.php index 98bbc32..b15b647 100755 --- a/Interfaces/DBInterface.php +++ b/Interfaces/DBInterface.php @@ -7,194 +7,15 @@ namespace MaplePHP\Query\Interfaces; +/** + * @method bind(\MaplePHP\Query\Prepare $param, array $statements) + */ interface DBInterface { /** - * Change where compare operator from default "=". - * Will change back to default after where method is triggered - * @param string $operator once of (">", ">=", "<", "<>", "!=", "<=", "<=>") - * @return self - */ - public function compare(string $operator): self; - - /** - * Chaining where with mysql "AND" or with "OR" - * @return self - */ - public function and(): self; - - /** - * Chaining where with mysql "AND" or with "OR" - * @return self - */ - public function or(): self; - - /** - * Access Query Attr class - * @param array|string|int|float $value - * @return AttrInterface - */ - public static function withAttr(array|string|int|float $value, ?array $args = null): AttrInterface; - - /** - * Raw Mysql Where input - * Uses vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually - * @param string $sql SQL string example: (id = %d AND permalink = '%s') - * @param array $arr Mysql prep values - * @return self - */ - 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 "=". - * @return self - */ - public function where(string|AttrInterface $key, string|int|float|AttrInterface $val, ?string $operator = null): self; - - - /** - * Group mysql WHERE inputs - * @param callable $call Evere method where placed inside callback will be grouped. - * @return self - */ - 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 "=". - * @return self - */ - public function having(string|AttrInterface $key, string|int|float|AttrInterface $val, ?string $operator = null): self; - - /** - * Raw Mysql HAVING input - * Uses vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually - * @param string $sql SQL string example: (id = %d AND permalink = '%s') - * @param array $arr Mysql prep values - * @return self - */ - public function havingRaw(string $sql, ...$arr): self; - - /** - * Add a limit and maybee a offset - * @param int $limit - * @param int|null $offset - * @return self - */ - public function limit(int $limit, ?int $offset = null): self; - - - /** - * Add a offset (if limit is not set then it will automatically become "1"). - * @param int $offset - * @return self - */ - 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". - * @return self - */ - public function order(string|AttrInterface $col, string $sort = "ASC"): self; - - /** - * Raw Mysql ORDER input - * Uses vsprintf to mysql prep/protect input in string. Prep string values needs to be eclosed manually - * @param string $sql SQL string example: (id ASC, parent DESC) - * @param array $arr Mysql prep values - * @return self - */ - public function orderRaw(string $sql, ...$arr): self; - - - /** - * Add group - * @param mixed $columns - * @return self - */ - 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 - * @return self - */ - public function join(string|array|MigrateInterface $table, string|array $where = null, array $sprint = array(), string $type = "INNER"): self; - - - /** - * Add make query a distinct call - * @return self - */ - public function distinct(): self; - - - /** - * Exaplain the mysql query. Will tell you how you can make improvements - * @return self - */ - public function explain(): 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" - * @return self - */ - public function set(string|array|AttrInterface $key, string|array|AttrInterface $value = null): self; - - /** - * UPROTECTED: 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) - */ - public function setRaw(string $key, string $value): self; - - - /** - * Update if ID KEY is duplicate else insert - * @param string|array $key (string) "name" OR (array) ["id" => 1, "name" => "Lorem ipsum"] - * @param string|null $value If key is string then value will pair with key "Lorem ipsum" - * @return self - */ - public function onDupKey($key = null, ?string $value = null): self; - - - /** - * Union result - * @param DBInterface $inst - * @param bool $allowDuplicate UNION by default selects only distinct values. - * Use UNION ALL to also select duplicate values! - * @return self - */ - public function union(DBInterface $inst, bool $allowDuplicate = false): self; - - /** - * Union raw result, create union with raw SQL code - * @param string $sql - * @param bool $allowDuplicate UNION by default selects only distinct values. - * Use UNION ALL to also select duplicate values! - * @mixin AbstractDB - * @return self - */ - public function unionRaw(string $sql, bool $allowDuplicate = false): self; - - /** - * Genrate SQL string of current instance/query + * Generate SQL string of current instance/query * @return string */ public function sql(): string; + } diff --git a/Interfaces/HandlerInterface.php b/Interfaces/HandlerInterface.php index 068a189..1d6673d 100755 --- a/Interfaces/HandlerInterface.php +++ b/Interfaces/HandlerInterface.php @@ -36,10 +36,10 @@ public function hasConnection(): bool; /** * Connect to database - * @return mixed + * @return ConnectInterface * @throws ConnectException */ - public function execute(): mixed; + public function execute(): ConnectInterface; /** diff --git a/Interfaces/QueryBuilderInterface.php b/Interfaces/QueryBuilderInterface.php new file mode 100755 index 0000000..5787db1 --- /dev/null +++ b/Interfaces/QueryBuilderInterface.php @@ -0,0 +1,12 @@ +query = $db; + $this->sql = $db->prepare()->sql(); + $this->stmt = $db->getConnection()->prepare($this->sql); + } + + /** + * Access DB -> and Query + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call(string $name, array $arguments): mixed + { + $query = $this->prepExecute(); + if(!method_exists($query, $name)) { + throw new \BadMethodCallException("The method '$name' does not exist in " . get_class($query) . "."); + } + return $query->$name(...$arguments); + } + + /** + * @param DBInterface $db + * @return void + */ + public function bind(DBInterface $db): void + { + $this->statements[0] = $db->prepare(); + } + + /** + * Combine multiple + * This is a test and might be removed in future + * @param DBInterface $db + * @return void + */ + public function combine(DBInterface $db): void + { + $this->statements[] = $db->prepare(); + } + + /** + * Get SQL code + * @return string + */ + public function sql(): string + { + return $this->sql; + } + + /** + * Get STMT + * @return mixed + */ + public function getStmt() + { + return $this->stmt; + } + + /** + * Get bound keys + * @param int $length + * @return string + */ + public function getKeys(int $length): string + { + if(is_null($this->keys)) { + $this->keys = str_pad("", $length, "s"); + } + return $this->keys; + } + + /** + * This will the rest of the library that it expects a prepended call + * @return Query + */ + private function prepExecute(): Query + { + return $this->query->bind($this, $this->statements); + } + + /** + * Execute + * @return array|bool|object + * @throws ConnectException + */ + function execute(): object|bool|array + { + $query = $this->prepExecute(); + return $query->execute(); + } + +} diff --git a/Query.php b/Query.php index 603e5af..ed23513 100755 --- a/Query.php +++ b/Query.php @@ -1,24 +1,30 @@ sql = $sql; if ($sql instanceof DBInterface) { $this->sql = $sql->sql(); } - $this->connection = is_null($connection) ? Connect::getInstance() : $connection; + $this->connection = $connection; } public function setPluck(?string $pluck): void @@ -26,6 +32,13 @@ public function setPluck(?string $pluck): void $this->pluck = $pluck; } + public function bind($stmt, array $set): self + { + $this->stmt = $stmt; + $this->bind = $set; + return $this; + } + /** * Execute query result * @return object|array|bool @@ -33,6 +46,10 @@ public function setPluck(?string $pluck): void */ public function execute(): object|array|bool { + if(!is_null($this->bind)) { + return $this->executePrepare(); + } + if ($result = $this->connection->query($this->sql)) { return $result; } else { @@ -40,6 +57,27 @@ public function execute(): object|array|bool } } + /** + * Execute prepared query + * @return object|array|bool + */ + public function executePrepare(): object|array|bool + { + if(is_null($this->bind)) { + throw new BadMethodCallException("You need to bind parameters first to execute a prepare statement!"); + } + foreach ($this->bind as $bind) { + $ref = $bind->getQueryBuilder()->getSet(); + $length = count($ref); + if($length > 0) { + $this->stmt->getStmt()->bind_param($this->stmt->getKeys($length), ...$ref); + } + $this->stmt->getStmt()->execute(); + } + //$this->stmt->getStmt()->close(); + return $this->stmt->getStmt()->get_result(); + } + /** * Execute query result And fetch as object * @return bool|object|string @@ -51,7 +89,7 @@ public function get(): bool|object|string } /** - * SAME AS @get(): Execute query result And fetch as obejct + * SAME AS @get(): Execute query result And fetch as object * @return bool|object|string (Mysql result) * @throws ConnectException */ @@ -70,16 +108,39 @@ final public function obj(string $class = "stdClass", array $constructor_args = /** * 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 callback, make changes in query and if return then change key + * @param string $class + * @param array $constructor_args * @return array * @throws ConnectException */ final public function fetch(?callable $callback = null, string $class = "stdClass", array $constructor_args = []): array + { + $arr = []; + $result = $this->execute(); + if (is_array($result)) { + foreach($result as $resultItem) { + $arr = array_merge($arr, $this->fetchItem($resultItem, $callback, $class, $constructor_args)); + } + } else { + $arr = $this->fetchItem($result, $callback, $class, $constructor_args); + } + return $arr; + } + + /** + * fetch an item to be used in the main fetch method + * @param $result + * @param callable|null $callback + * @param string $class + * @param array $constructor_args + * @return array + */ + protected function fetchItem($result, ?callable $callback = null, string $class = "stdClass", array $constructor_args = []): array { $key = 0; $select = null; - $arr = array(); - $result = $this->execute(); + $arr = []; if (is_object($result) && $result->num_rows > 0) { while ($row = $result->fetch_object($class, $constructor_args)) { @@ -93,14 +154,12 @@ final public function fetch(?callable $callback = null, string $class = "stdClas $data = ((!is_null($select)) ? $select : $key); if (is_array($data)) { if (!is_array($select)) { - throw new \InvalidArgumentException("The return value of the callable needs to be an array!", 1); + throw new InvalidArgumentException("The return value of the callable needs to be an array!", 1); } $arr = array_replace_recursive($arr, $select); } else { - $arr[$data] = $row; } - $key++; } } diff --git a/QueryBuilder.php b/QueryBuilder.php new file mode 100755 index 0000000..15d3c10 --- /dev/null +++ b/QueryBuilder.php @@ -0,0 +1,271 @@ +db = $sql; + } + + public function __toString(): string + { + return $this->sql(); + } + + public function select(): string + { + $explain = $this->getExplain(); + $noCache = $this->getNoCache(); + $columns = $this->getColumns(); + $distinct = $this->getDistinct(); + $join = $this->getJoin(); + $where = $this->getWhere("WHERE", $this->db->where); + $having = $this->getWhere("HAVING", $this->db->having); + $order = $this->getOrder(); + $limit = $this->getLimit(); + $group = $this->getGroup(); + $union = $this->getUnion(); + + return "{$explain}SELECT $noCache$distinct$columns FROM " . + $this->getTable() . "$join$where$group$having$order$limit$union"; + } + + public function getTable(): string + { + return Helpers::addAlias($this->db->table, $this->db->alias); + } + + /** + * Get sql code + * @return string + */ + public function sql(): string + { + return $this->select(); + } + + /** + * Optimizing Queries with EXPLAIN + * @return string + */ + protected function getExplain(): string + { + return ($this->db->explain) ? "EXPLAIN " : ""; + } + + /** + * The SELECT DISTINCT statement is used to return only distinct (different) values + * @return string + */ + protected function getDistinct(): string + { + return ($this->db->distinct) ? "DISTINCT " : ""; + } + + /** + * The server does not use the query cache. + * @return string + */ + protected function getNoCache(): string + { + return ($this->db->noCache) ? "SQL_NO_CACHE " : ""; + } + + /** + * The SELECT columns + * @return string + */ + protected function getColumns(): string + { + if(is_null($this->db->columns)) { + return "*"; + } + $create = []; + $columns = $this->db->columns; + foreach($columns as $row) { + $create[] = Helpers::addAlias($row['column'], $row['alias'], "AS"); + } + return implode(",", $create); + } + + /** + * Order rows by + * @return string + */ + protected function getOrder(): string + { + return (!is_null($this->db->order)) ? + " ORDER BY " . implode(",", Helpers::getOrderBy($this->db->order)) : ""; + } + + /** + * The GROUP BY statement groups rows that have the same values into summary rows + * @return string + */ + protected function getGroup(): string + { + return (!is_null($this->db->group)) ? " GROUP BY " . implode(",", $this->db->group) : ""; + } + + /** + * Will build where string + * @param string $prefix + * @param array|null $where + * @param array $set + * @return string + */ + protected function getWhere(string $prefix, ?array $where, array &$set = []): 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, $set); + $out .= ")"; + $index++; + } + } + return $out; + } + + /** + * Build joins + * @return string + */ + protected function getJoin(): string + { + $join = ""; + $data = $this->db->join; + foreach ($data as $row) { + $table = Helpers::addAlias($row['table'], $row['alias']); + $where = $this->getWhere("ON", $row['whereData']); + $join .= " ". sprintf("%s JOIN %s%s", $row['type'], $table, $where); + } + return $join; + } + + /** + * Build limit + * @return string + */ + protected function getLimit(): string + { + $limit = $this->db->limit; + if (is_null($limit) && !is_null($this->db->offset)) { + $limit = 1; + } + $limit = $this->getAttrValue($limit); + $offset = (!is_null($this->db->offset)) ? "," . $this->getAttrValue($this->db->offset) : ""; + return (!is_null($limit)) ? " LIMIT $limit $offset" : ""; + } + + /** + * Build Where data (CAN BE A HELPER?) + * @param array $array + * @param array $set + * @return string + */ + private function whereArrToStr(array $array, array &$set = []): string + { + $out = ""; + $count = 0; + foreach ($array as $key => $arr) { + foreach ($arr as $arrB) { + if (is_array($arrB)) { + foreach ($arrB as $row) { + if ($count > 0) { + $out .= "$key "; + } + if ($row['not'] === true) { + $out .= "NOT "; + } + + $value = $this->getAttrValue($row['value']); + $out .= "{$row['column']} {$row['operator']} {$value} "; + $set[] = $row['value']; + $count++; + } + + } else { + // Used to be used as RAW input but is not needed any more + die("DELETE???"); + $out .= ($count) > 0 ? "$key $arrB " : $arrB; + $count++; + } + } + } + return rtrim($out, " "); + } + + + /** + * Get Union sql + * @return string + */ + public function getUnion(): string + { + $union = $this->db->union; + if(!is_null($union)) { + + $sql = ""; + foreach($union as $row) { + $inst = new self($row['inst']); + $sql .= " UNION " . $inst->sql(); + } + + return $sql; + } + return ""; + } + + /** + * Get attribute as value item + * @param $value + * @return string|null + */ + public function getAttrValue($value): ?string + { + if($this->db->prepare) { + if($value instanceof AttrInterface && ($value->isType(Attr::VALUE_TYPE) || + $value->isType(Attr::VALUE_TYPE_NUM) || $value->isType(Attr::VALUE_TYPE_STR))) { + $this->set[] = $value->type(Attr::RAW_TYPE); + return "?"; + } + } + return is_null($value) ? null : (string)$value; + } + + /** + * Get set + * @return array + */ + public function getSet(): array + { + return $this->set; + } + +} diff --git a/QueryBuilderLegacy.php b/QueryBuilderLegacy.php new file mode 100755 index 0000000..e161758 --- /dev/null +++ b/QueryBuilderLegacy.php @@ -0,0 +1,274 @@ +db = $sql; + } + + public function __toString(): string + { + return $this->sql(); + } + + public function select(): string + { + $explain = $this->getExplain(); + $noCache = $this->getNoCache(); + $columns = $this->getColumns(); + $distinct = $this->getDistinct(); + $join = $this->getJoin(); + $where = $this->getWhere("WHERE", $this->db->__get('where')); + $having = $this->getWhere("HAVING", $this->db->__get('having')); + $order = $this->getOrder(); + $limit = $this->getLimit(); + $group = $this->getGroup(); + $union = $this->getUnion(); + + return "{$explain}SELECT $noCache$distinct$columns FROM " . + $this->getTable() . "$join$where$group$having$order$limit$union"; + } + + public function getTable(): string + { + return Helpers::addAlias($this->db->__get('table'), $this->db->__get('alias')); + } + + /** + * Get sql code + * @return string + */ + public function sql(): string + { + return $this->select(); + } + + /** + * Optimizing Queries with EXPLAIN + * @return string + */ + protected function getExplain(): string + { + return ($this->db->__get('explain')) ? "EXPLAIN " : ""; + } + + /** + * The SELECT DISTINCT statement is used to return only distinct (different) values + * @return string + */ + protected function getDistinct(): string + { + return ($this->db->__get('distinct')) ? "DISTINCT " : ""; + } + + /** + * The server does not use the query cache. + * @return string + */ + protected function getNoCache(): string + { + return ($this->db->__get('noCache')) ? "SQL_NO_CACHE " : ""; + } + + /** + * The SELECT columns + * @return string + */ + protected function getColumns(): string + { + if(is_null($this->db->__get('columns'))) { + return "*"; + } + $create = []; + $columns = $this->db->__get('columns'); + foreach($columns as $row) { + $create[] = Helpers::addAlias($row['column'], $row['alias'], "AS"); + } + return implode(",", $create); + } + + /** + * Order rows by + * @return string + */ + protected function getOrder(): string + { + return (!is_null($this->db->__get('order'))) ? + " ORDER BY " . implode(",", Helpers::getOrderBy($this->db->__get('order'))) : ""; + } + + /** + * The GROUP BY statement groups rows that have the same values into summary rows + * @return string + */ + protected function getGroup(): string + { + return (!is_null($this->db->__get('group'))) ? " GROUP BY " . implode(",", $this->db->__get('group')) : ""; + } + + /** + * Will build where string + * @param string $prefix + * @param array|null $where + * @param array $set + * @return string + */ + protected function getWhere(string $prefix, ?array $where, array &$set = []): 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, $set); + $out .= ")"; + $index++; + } + } + return $out; + } + + /** + * Build joins + * @return string + */ + protected function getJoin(): string + { + $join = ""; + $data = $this->db->__get("join"); + foreach ($data as $row) { + $table = Helpers::addAlias($row['table'], $row['alias']); + $where = $this->getWhere("ON", $row['whereData']); + $join .= " ". sprintf("%s JOIN %s%s", $row['type'], $table, $where); + } + return $join; + } + + /** + * Build limit + * @return string + */ + protected function getLimit(): string + { + $limit = $this->db->__get('limit'); + if (is_null($limit) && !is_null($this->db->__get('offset'))) { + $limit = 1; + } + $limit = $this->getAttrValue($limit); + $offset = (!is_null($this->db->__get("offset"))) ? "," . $this->getAttrValue($this->db->__get("offset")) : ""; + return (!is_null($limit)) ? " LIMIT $limit $offset" : ""; + } + + /** + * Build Where data (CAN BE A HELPER?) + * @param array $array + * @param array $set + * @return string + */ + private function whereArrToStr(array $array, array &$set = []): string + { + $out = ""; + $count = 0; + foreach ($array as $key => $arr) { + foreach ($arr as $arrB) { + if (is_array($arrB)) { + foreach ($arrB as $row) { + if ($count > 0) { + $out .= "$key "; + } + if ($row['not'] === true) { + $out .= "NOT "; + } + + $value = $this->getAttrValue($row['value']); + $out .= "{$row['column']} {$row['operator']} {$value} "; + $set[] = $row['value']; + $count++; + } + + } else { + // Used to be used as RAW input but is not needed any more + die("DELETE???"); + $out .= ($count) > 0 ? "$key $arrB " : $arrB; + $count++; + } + } + } + return rtrim($out, " "); + } + + + /** + * Get Union sql + * @return string + */ + public function getUnion(): string + { + $union = $this->db->__get('union'); + if(!is_null($union)) { + + $sql = ""; + foreach($union as $row) { + $inst = new self($row['inst']); + $sql .= " UNION " . $inst->sql(); + } + + return $sql; + } + return ""; + } + + /** + * Get attribute as value item + * @param $value + * @return string|null + */ + public function getAttrValue($value): ?string + { + if($this->db->__get('prepare')) { + if($value instanceof AttrInterface && ($value->isType(Attr::VALUE_TYPE) || + $value->isType(Attr::VALUE_TYPE_NUM) || $value->isType(Attr::VALUE_TYPE_STR))) { + $this->set[] = $value->type(Attr::RAW_TYPE); + return "?"; + } + } + return is_null($value) ? null : (string)$value; + } + + /** + * Get set + * @return array + */ + public function getSet(): array + { + if(!$this->db->__get('prepare')) { + throw new RuntimeException("Prepare method not available"); + } + return $this->set; + } + +} diff --git a/Utility/Attr.php b/Utility/Attr.php index 3dbe363..04f87f5 100755 --- a/Utility/Attr.php +++ b/Utility/Attr.php @@ -2,43 +2,49 @@ namespace MaplePHP\Query\Utility; -use MaplePHP\Query\Connect; +use BadMethodCallException; +use InvalidArgumentException; +use MaplePHP\Query\Handlers\PostgreSQL\PostgreSQLConnect; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\DTO\Format\Encode; +use MaplePHP\Query\Interfaces\ConnectInterface; +use MaplePHP\Query\Interfaces\HandlerInterface; +/** + * I might make it IMMUTABLE in future + */ class Attr implements AttrInterface { - private $value; - private $raw; - private $hasBeenEncoded = false; - private $prep = true; - private $enclose = true; - private $jsonEncode = true; - private $encode = false; + public const RAW_TYPE = 0; + public const VALUE_TYPE = 1; + public const COLUMN_TYPE = 2; + public const VALUE_TYPE_NUM = 3; + public const VALUE_TYPE_STR = 4; - /** - * Initiate the instance - * @param array|string|int|float $value - */ - public function __construct($value) - { - $this->value = $value; - $this->raw = $value; - } + private ConnectInterface|HandlerInterface $connection; + private float|int|array|string|null $value = null; + private float|int|array|string $raw; + //private array $set = []; + //private bool $hasBeenEncoded = false; + + private int $type = 0; + private bool $prep = true; + private bool $sanitize = true; + private bool $enclose = true; + private bool $jsonEncode = false; + private bool $encode = false; /** * Initiate the instance - * @param array|string|int|float $value - * @return self + * @param ConnectInterface|HandlerInterface $connection */ - public static function value(array|string|int|float $value): self + public function __construct(ConnectInterface|HandlerInterface $connection) { - $inst = new self($value); - return $inst; + $this->connection = $connection; } /** - * Process string after your choises + * Process string after your choices * @return string */ public function __toString(): string @@ -47,88 +53,177 @@ public function __toString(): string } /** - * Will escape and encode values the right way buy the default - * If prepped then quotes will be escaped and not encoded - * If prepped is diabled then quotes will be encoded - * @return string + * IMMUTABLE: Set value you that want to encode against. + * Will also REST all values to its defaults + * @param float|int|array|string $value + * @return self */ - public function getValue(): string + public function withValue(float|int|array|string $value): self { - if (!$this->hasBeenEncoded) { - - $this->hasBeenEncoded = true; - $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); - } - - if($this->prep) { - $this->value = Connect::getInstance()->prep($this->value); - } + $inst = new self($this->connection); + $inst->value = $value; + $inst->raw = $value; + return $inst; + } - if ($this->enclose) { - $this->value = "'{$this->value}'"; - } + public function type(int $dataType): static + { + $inst = clone $this; + if($dataType < 0 || $dataType > self::VALUE_TYPE_STR) { + throw new InvalidArgumentException('The data type expects to be either "RAW_TYPE (0), VALUE_TYPE (1), + COLUMN_TYPE (2), VALUE_TYPE_NUM (3), VALUE_TYPE_STR (4)"!'); + } + $inst->type = $dataType; + if($dataType === self::RAW_TYPE) { + $inst = $inst->prep(false)->enclose(false)->encode(false)->sanitize(false); } - return $this->value; + + if($dataType === self::VALUE_TYPE_NUM) { + $inst = $inst->enclose(false); + $inst->value = (float)$inst->value; + } + + // Will not "prep" column type by default, but instead it will "sanitize" + if($dataType === self::COLUMN_TYPE) { + $inst = $inst->prep(false)->sanitize(true); + } + return $inst; } /** - * Get raw data from instance - * @return string|array + * Check value type + * @param int $type + * @return int */ - public function getRaw(): string|array + public function isType(int $type): int { - return $this->raw; + return ($this->type === $type); } /** - * Enable/disable MySQL prep + * IMMUTABLE: Enable/disable MySQL prep * @param bool $prep - * @return self + * @return static */ - public function prep(bool $prep): self + public function prep(bool $prep): static { - $this->prep = $prep; - return $this; + $inst = clone $this; + $inst->prep = $prep; + return $inst; } /** - * Enable/disable string enclose + * Sanitize column types + * @param bool $sanitize + * @return $this + */ + public function sanitize(bool $sanitize): static + { + $inst = clone $this; + $inst->sanitize = $sanitize; + return $inst; + } + + /** + * CHANGE name to RAW? + * IMMUTABLE: Enable/disable string enclose * @param bool $enclose - * @return self + * @return static */ - public function enclose(bool $enclose): self + public function enclose(bool $enclose): static { - $this->enclose = $enclose; - return $this; + $inst = clone $this; + $inst->enclose = $enclose; + return $inst; } /** - * If Request[key] is array then auto convert it to json to make it database ready + * IMMUTABLE: If Request[key] is array then auto convert it to json to make it database ready * @param bool $jsonEncode - * @return self + * @return static */ - public function jsonEncode(bool $jsonEncode): self + public function jsonEncode(bool $jsonEncode): static { - $this->jsonEncode = $jsonEncode; - return $this; + $inst = clone $this; + $inst->jsonEncode = $jsonEncode; + return $inst; } /** - * Enable/disable XSS protection + * CHANGE name to special char?? + * IMMUTABLE: Enable/disable XSS protection * @param bool $encode (default true) - * @return self + * @return static + */ + public function encode(bool $encode): static + { + $inst = clone $this; + $inst->encode = $encode; + return $inst; + } + + /** + * CHANGE NAME TO GET?? + * Can only be encoded once + * Will escape and encode values the right way buy the default + * If prepped then quotes will be escaped and not encoded + * If prepped is disabled then quotes will be encoded + * @return string */ - public function encode(bool $encode): self + public function getValue(): string { - $this->encode = $encode; - return $this; + $inst = clone $this; + if(is_null($inst->value)) { + throw new BadMethodCallException("You need to set a value first with \"withValue\""); + } + + $inst->value = Encode::value($inst->value) + ->specialChar($inst->encode, ($inst->prep ? ENT_NOQUOTES : ENT_QUOTES)) + ->sanitizeIdentifiers($inst->sanitize) + ->urlEncode(false) + ->encode(); + + // Array values will automatically be json encoded + if ($inst->jsonEncode || is_array($inst->value)) { + // If prep is on then escape after json_encode, + // otherwise json encode will possibly escape the escaped value + $inst->value = json_encode($inst->value); + } + + if($inst->prep) { + $inst->value = $inst->connection->prep($inst->value); + } + + if ($inst->enclose) { + // Do not use backticks in PostgreSQL + if(!(($inst->connection instanceof PostgreSQLConnect) && $inst->type === self::COLUMN_TYPE)) { + $inst->value = ($inst->type === self::COLUMN_TYPE) ? $this->getValueToColumn() : "'$inst->value'"; + } + } + + return $inst->value; + } + + /** + * Will convert a value to a column type + * @return string + */ + protected function getValueToColumn(): string + { + $arr = []; + $exp = explode('.', $this->value); + foreach($exp as $value) { + $arr[] = "`$value`"; + } + return implode('.', $arr); + } + + /** + * Get raw data from instance + * @return string|array + */ + public function getRaw(): string|array + { + return $this->raw; } } diff --git a/Utility/Build.php b/Utility/Build.php deleted file mode 100755 index 6a39a1a..0000000 --- a/Utility/Build.php +++ /dev/null @@ -1,309 +0,0 @@ -", ">=", "<", "<>", "!=", "<=", "<=>"]; // 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::getInstance()->getHandler()->getPrefix(); - $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::getInstance()->getHandler()->getPrefix(); - $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"; - } -} diff --git a/Utility/Helpers.php b/Utility/Helpers.php new file mode 100644 index 0000000..90a5ae3 --- /dev/null +++ b/Utility/Helpers.php @@ -0,0 +1,241 @@ +", ">=", "<", "<>", "!=", "<=", "<=>"]; // Comparison operators + protected const JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "CROSS"]; // Join types + + + /** + * Whitelist comparison operators + * @param string $val + * @return string + */ + public static function operator(string $val): string + { + $val = trim($val); + if (in_array($val, static::OPERATORS)) { + return $val; + } + return "="; + } + + /** + * Whitelist mysql sort directions + * @param string $val + * @return string + */ + public static function orderSort(string $val): string + { + $val = strtoupper($val); + if ($val === "ASC" || $val === "DESC") { + return $val; + } + return "ASC"; + } + + /** + * Whitelist mysql join types + * @param string $val + * @return string + */ + public static function joinTypes(string $val): string + { + $val = trim($val); + if (in_array($val, static::JOIN_TYPES)) { + return $val; + } + return "INNER"; + } + + /** + * Prepare order by + * @param array $arr + * @return array + */ + public static function getOrderBy(array $arr): array + { + $new = []; + foreach($arr as $row) { + $new[] = "{$row['column']} {$row['sort']}"; + } + return $new; + } + + + public static function buildJoinData(DBInterface $inst, string|array $where): array + { + $data = []; + if (is_array($where)) { + foreach ($where as $key => $val) { + if (is_array($val)) { + foreach ($val as $grpKey => $grpVal) { + if(!($grpVal instanceof AttrInterface)) { + $grpVal = "%s"; + } + $inst->setWhereData($grpKey, $grpVal, $data); + } + } else { + if(!($val instanceof AttrInterface)) { + $val = "%s"; + } + $inst->setWhereData($key, $val, $data); + } + } + } + + return $data; + } + + public function validateIdentifiers($column): bool + { + return (preg_match('/^[a-zA-Z0-9_]+$/', $column) !== false); + } + + /** + * Mysql Prep/protect string + * @param mixed $val + * @param bool $enclose + * @return AttrInterface + */ + public static function prep(mixed $val, bool $enclose = true): AttrInterface + { + if ($val instanceof AttrInterface) { + return $val; + } + $val = static::getAttr($val); + $val->enclose($enclose); + return $val; + } + + /** + * Mysql Prep/protect array items + * @param array $arr + * @param bool $enclose + * @return array + */ + public static function prepArr(array $arr, bool $enclose = true): array + { + $new = []; + foreach ($arr as $pKey => $pVal) { + $key = (string)static::prep($pKey, false); + $new[$key] = (string)static::prep($pVal, $enclose); + } + return $new; + } + + /** + * MOVE TO DTO ARR + * Will extract camelcase to array + * @param string $value string value with possible camel cases + * @return array + */ + public static function extractCamelCase(string $value): array + { + return preg_split('#([A-Z][^A-Z]*)#', $value, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + } + + /** + * Use to loop camel case method columns + * @param array $camelCaseArr + * @param array $valArr + * @param callable $call + * @return void + */ + public static function camelLoop(array $camelCaseArr, array $valArr, callable $call): void + { + foreach ($camelCaseArr as $k => $col) { + $col = lcfirst($col); + $value = ($valArr[$k] ?? null); + $call($col, $value); + } + } + + /** + * Get new Attr instance + * @param array|string|int|float $value + * @return AttrInterface + */ + public static function getAttr(array|string|int|float $value): AttrInterface + { + return new Attr($value); + } + + /** + * Access Query Attr class + * @param array|string|int|float $value + * @param array|null $args + * @return AttrInterface + */ + public static function withAttr(array|string|int|float $value, ?array $args = null): AttrInterface + { + $inst = static::getAttr($value); + if (!is_null($args)) { + foreach ($args as $method => $arg) { + if (!method_exists($inst, $method)) { + throw new BadMethodCallException("The Query Attr method \"" .htmlspecialchars($method, ENT_QUOTES). "\" does not exists!", 1); + } + $inst = call_user_func_array([$inst, $method], (!is_array($arg) ? [$arg] : $arg)); + } + } + return $inst; + } + + /** + * Separate Alias + * @param string|array $data + * @return array + * @throws InvalidArgumentException + */ + public static function separateAlias(string|array $data): array + { + $alias = null; + $table = $data; + if (is_array($data)) { + if (count($data) !== 2) { + throw new InvalidArgumentException("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]; + } + + + /** + * Will add a alias to a MySQL table + * @param string $table + * @param string|null $alias + * @return string + */ + public static function addAlias(string|AttrInterface $table, null|string|AttrInterface $alias = null, string $command = ""): string + { + if(!is_null($alias)) { + $table .= ($command ? " {$command} " : " ") . $alias; + } + return $table; + } + + /** + * Will add a alias to a MySQL table + * @param string $table + * @param string|null $alias + * @return string + */ + public static function toAlias(string|AttrInterface $table, null|string|AttrInterface $alias = null): string + { + if(!is_null($alias)) { + $table .= " AS " . $alias; + } + return $table; + } + +} diff --git a/Utility/WhitelistMigration.php b/Utility/WhitelistMigration.php index a8ee6fe..4385016 100755 --- a/Utility/WhitelistMigration.php +++ b/Utility/WhitelistMigration.php @@ -1,12 +1,11 @@ data[$key])) { - $this->message = "The column ({$key}) do not exists in database table."; + $this->message = "The column ($key) do not exists in database table."; return false; } @@ -130,10 +131,10 @@ public function where(string $key, AttrInterface $value): bool $length = (int)($this->data[$key]['length'] ?? 0); if ((in_array($type, self::TYPE_NUMERIC) && !is_numeric($value)) || !is_string($value)) { - $this->message = "The database column ({$key}) value type is not supported."; + $this->message = "The database column ($key) value type is not supported."; return false; } elseif ($length < strlen($value)) { - $this->message = "The database column ({$key}) value length is longer than ({$length})."; + $this->message = "The database column ($key) value length is longer than ($length)."; return false; } return true; diff --git a/composer.json b/composer.json index 9df6b57..db2f46a 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ ], "require": { "php": ">=8.0", - "maplephp/dto": "^1.0" + "maplephp/dto": "^2.0" }, "require-dev": { "maplephp/unitary": "^1.0" diff --git a/tests/database.sqlite b/tests/database.sqlite index aa5c121..959534d 100644 Binary files a/tests/database.sqlite and b/tests/database.sqlite differ diff --git a/tests/unitary-database.php b/tests/unitary-database.php new file mode 100755 index 0000000..33a8224 --- /dev/null +++ b/tests/unitary-database.php @@ -0,0 +1,130 @@ +add("Unitary test 3333", function () use ($unit) { + + //$handler = new MySQLHandler(getenv("DATABASE_HOST"), getenv("DATABASE_USERNAME"), getenv("DATABASE_PASSWORD"), "test"); + //$handler = new PostgreSQLHandler("127.0.0.1", "postgres", "", "postgres"); + $handler = new SQLiteHandler(__DIR__ . "/database.sqlite"); + $handler->setPrefix("maple_"); + $db = new DBTest($handler); + + + + + //$p1 = $db->table("test")->where("parent", 0); + + + /* + for($i = 0; $i < 80000; $i++) { + //$db->getConnection()->query("SELECT * FROM maple_test WHERE parent=0"); + //$p1->query("SELECT * FROM maple_test WHERE parent=0")->execute(); + //$p1->query($p1->sql())->execute(); + //$p1->execute(); + } + */ + + + + + /* + $value = '0'; + $stmt = $db->getConnection()->prepare("SELECT * FROM maple_test WHERE parent=?"); + for($i = 0; $i < 80000; $i++) { + $stmt->bind_param('s', $value); + $value = '1'; + $stmt->execute(); + } + $result = $stmt->get_result(); + $stmt->close(); + */ + + $startTime = microtime(true); + $startMemory = memory_get_usage(); + $p1 = $db->table("test")->where("parent", 1); + $unit->performance(function() use ($db) { + $p1 = $db->table("test")->where("parent", 1); + $prepare = new Prepare($p1); + for($i = 0; $i < 40000; $i++) { + $prepare->bind($p1->where("parent", 1)); + $prepare->execute(); + } + }); + + + $p1 = $db->table("test")->where("parent", 1); + $unit->performance(function() use ($db) { + $p1 = $db->table("test")->where("parent", 1); + $prepare = new Prepare($p1); + for($i = 0; $i < 1000; $i++) { + $prepare->bind($p1->where("parent", 1)); + $prepare->execute(); + } + }); + + $p1 = $db->table("test")->where("parent", 1); + $unit->performance(function() use ($db) { + $p1 = $db->table("test")->where("parent", 1); + $prepare = new Prepare($p1); + for($i = 0; $i < 10000; $i++) { + $prepare->bind($p1->where("parent", 1)); + $prepare->execute(); + } + }); + + + + /* + * $prepare = new Prepare($p1); + for($i = 0; $i < 10000; $i++) { + $prepare->bind($p1->where("parent", 1)); + $prepare->execute(); + } + */ + + + + /* + + + + for($i = 0; $i < 50000; $i++) { + $p1->execute(); + } + */ + + + + die; + + /* + Execution time: 28.407850027084 seconds +Memory used: 2291.7578125 KB +Peak memory used: 5136.6484375 KB + */ + + $this->add("Lorem ipsum dolor", [ + "isInt" => [], + "length" => [1,200] + + ])->add(92928, [ + "isInt" => [] + + ])->add("Lorem", [ + "isString" => [], + "length" => function () { + return $this->length(1, 50); + } + + ], "The length is not correct!"); + +}); + diff --git a/tests/unitary-db.php b/tests/unitary-db.php index c42546c..65f0145 100755 --- a/tests/unitary-db.php +++ b/tests/unitary-db.php @@ -2,104 +2,118 @@ use database\migrations\Test; use database\migrations\TestCategory; +use MaplePHP\Query\DBTest; +use MaplePHP\Query\Handlers\MySQLHandler; use MaplePHP\Query\Handlers\PostgreSQLHandler; use MaplePHP\Query\Handlers\SQLiteHandler; +use MaplePHP\Query\Prepare; +use MaplePHP\Query\Utility\Attr; use MaplePHP\Unitary\Unit; use MaplePHP\Query\Connect; -use MaplePHP\Query\DB; + // Only validate if there is a connection open! -if (Connect::hasInstance() && Connect::getInstance()->hasConnection()) { +//if (Connect::hasInstance() && Connect::getInstance()->hasConnection()) { $unit = new Unit(); + $handler = new MySQLHandler(getenv("DATABASE_HOST"), getenv("DATABASE_USERNAME"), getenv("DATABASE_PASSWORD"), "test"); + $handler->setPrefix("maple_"); + $db = new DBTest($handler); - // 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, [ + + + + + $unit->case("OK", function () use ($unit) { + + $this->add("Test", [ "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"); - }); + "length" => [1, 200] + ]); - /** - * 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->add("Testwqd", [ + "length" => [1, 200] + ], "Test data type"); - /** - * 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(); -} +//} + + +/* + + $instances = [null, "postgresql", "sqlite"]; + + $sqLiteHandler = new PostgreSQLHandler("127.0.0.1", "postgres", "", "postgres"); + $sqLiteHandler->setPrefix("maple_"); + $connect = Connect::setHandler($sqLiteHandler, "postgresql"); + $connect->execute(); + + $sqLiteHandler = new SQLiteHandler(__DIR__ . "/database.sqlite"); + $sqLiteHandler->setPrefix("maple_"); + $connect = Connect::setHandler($sqLiteHandler, "sqlite"); + $connect->execute(); + + + // Add a title to your tests (not required) + $unit->addTitle("Testing MaplePHP Query library!"); + foreach($instances as $key) { + $message = "Error in " . (is_null($key) ? "mysql" : $key); + $unit->add($message, function ($inst) use ($unit, $key, $instances) { + + // Select handler + $db = Connect::getInstance($key); + + $inst->add($db->hasConnection(), [ + "equal" => [true], + ], "Missing connection"); + + $select = $db::select("test.id", "test") + ->whereBind(function ($inst) { + $inst->not() + ->where("status", 0) + ->or() + ->where("status", 0, ">"); + }) + ->whereParent(0) + ->having("id", 0, ">") + ->whereRaw("id > 0") + ->havingRaw("COUNT(id) > 0") + ->group("id") + ->distinct("id") + ->limit(2); + $select->join(["test_category", "b"], "tid = id"); + $arr = $select->fetch(); + + + //$unit->command()->message($select->sql()); + $inst->add(count($arr), [ + "equal" => [2], + ], "Data is missing"); + + + // Test union + $union = $db::select("id,name", "test"); + $unit->command()->message(Connect::$current); + + $select = $db::select("cat_id AS id,name", "test_category"); + + + + $insert = $db::insert("test")->set([ + "name" => "Test row", + "content" => "Delete this row", + "status" => 1, + "parent" => 0, + "create_date" => date("Y-m-d H:i:s", time()), + ]); + + + + + //print_r($db->connection()); + //$insert->execute(); + + }); + } + */ \ No newline at end of file