From 6dbec84ea07f81b758b274ecba47c583bebc9bbe Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Wed, 28 Aug 2024 20:41:09 +0200 Subject: [PATCH 1/3] Core updates --- AbstractDB.php | 70 ++- Connect.php | 300 ++++++------ ConnectTest.php | 42 ++ Create.php | 4 +- DB.php | 155 +++--- DBTest.php | 461 ++++++++++++++++++ ...QueryException.php => ResultException.php} | 4 +- Handlers/MySQL/MySQLConnect.php | 70 +++ Handlers/MySQLHandler.php | 23 +- Handlers/PostgreSQL/PostgreSQLConnect.php | 51 +- Handlers/PostgreSQLHandler.php | 23 +- Handlers/SQLite/SQLiteConnect.php | 24 +- Handlers/SQLite/SQLiteResult.php | 11 +- Handlers/SQLiteHandler.php | 25 +- Interfaces/ConnectInterface.php | 11 +- Interfaces/HandlerInterface.php | 4 +- Utility/Attr.php | 28 +- Utility/Build.php | 309 ------------ Utility/Helpers.php | 170 +++++++ Utility/WhitelistMigration.php | 24 +- tests/database.sqlite | Bin 8192 -> 20480 bytes tests/unitary-db.php | 182 +++---- 22 files changed, 1255 insertions(+), 736 deletions(-) create mode 100755 ConnectTest.php create mode 100755 DBTest.php rename Exceptions/{DBQueryException.php => ResultException.php} (62%) create mode 100644 Handlers/MySQL/MySQLConnect.php delete mode 100755 Utility/Build.php create mode 100644 Utility/Helpers.php diff --git a/AbstractDB.php b/AbstractDB.php index e25cfbe..7989c67 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -4,12 +4,13 @@ namespace MaplePHP\Query; use MaplePHP\Query\Exceptions\ConnectException; +use MaplePHP\Query\Interfaces\ConnectInterface; use MaplePHP\Query\Utility\Attr; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\MigrateInterface; use MaplePHP\Query\Interfaces\DBInterface; use MaplePHP\Query\Exceptions\DBValidationException; -use MaplePHP\Query\Exceptions\DBQueryException; +use MaplePHP\Query\Exceptions\ResultException; /** * @psalm-taint-source @@ -32,7 +33,7 @@ abstract class AbstractDB implements DBInterface protected $whereProtocol = []; protected $fkData; protected $joinedTables; - + protected ?string $pluck = null; protected string $connKey = "default"; /** @@ -85,36 +86,51 @@ abstract protected function dropView(): self; */ abstract protected function showView(): self; - public function setConnKey(?string $key) { + + /** + * Set connection + * @param string|null $key + * @return void + */ + public function setConnKey(?string $key): void + { $this->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 +153,6 @@ protected function getAttr(array|string|int|float $value): AttrInterface return new Attr($value); } - /** * Will reset Where input * @return void @@ -191,17 +206,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,10 +227,11 @@ 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 { @@ -228,12 +245,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,24 +268,23 @@ 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; } @@ -324,7 +340,7 @@ 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 @@ -351,7 +367,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 */ @@ -403,6 +420,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 +438,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,7 +448,7 @@ 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; } diff --git a/Connect.php b/Connect.php index 9eb0bef..e07ef9a 100755 --- a/Connect.php +++ b/Connect.php @@ -3,34 +3,85 @@ namespace MaplePHP\Query; +use Exception; use InvalidArgumentException; use MaplePHP\Query\Exceptions\ConnectException; -use MaplePHP\Query\Interfaces\AttrInterface; +use MaplePHP\Query\Exceptions\ResultException; +use MaplePHP\Query\Interfaces\HandlerInterface; +use MaplePHP\Query\Interfaces\ConnectInterface; use MaplePHP\Query\Interfaces\MigrateInterface; -use MaplePHP\Query\Utility\Attr; -use mysqli; /** + * Connect a singleton connection class + * + * WIll not be __callStatic in future! * @method static select(string $columns, string|array|MigrateInterface $table) * @method static table(string $string) + * @method static insert(string $string) */ -class Connect +class Connect implements ConnectInterface { - private $handler; + private HandlerInterface $handler; private static array $inst; public static string $current = "default"; - private $db; + private ?ConnectInterface $connection = null; - private function __construct($handler) + /** + * @param HandlerInterface $handler + */ + private function __construct(HandlerInterface $handler) { $this->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 + { + + /* + var_dump("WTFTTTT", $key); + echo "\n\n\n"; + $beg = debug_backtrace(); + foreach($beg as $test) { + var_dump(($test['file'] ?? "noFile"), ($test['line'] ?? "noLine"), ($test['class'] ?? "noClass"), ($test['function'] ?? "noFunction")); + } + */ + + + $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 +93,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 +132,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 +144,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 + function getHandler(): HandlerInterface { - return $this->handler->getPrefix(); + return $this->handler; } /** @@ -154,93 +194,129 @@ 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 + 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 + 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 + 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 */ + function insert_id(?string $column = null): int + { + return $this->connection->insert_id($column); + } /** - * Execute multiple quries at once (e.g. from a sql file) - * @param string $sql - * @param object|null &$mysqli - * @return array + * Close connection + * @return bool */ - public function multiQuery(string $sql, object &$mysqli = null): array + function close(): true { - return $this->handler->multiQuery($sql, $mysqli); + return $this->connection->close(); } /** - * Start Transaction - * @return mysqli + * Start Transaction will return instance of ConnectInterface instead of bool + * @return ConnectInterface + * @throws ConnectException */ - public function transaction(): mixed + public function transaction(): ConnectInterface { - return $this->handler->transaction(); + 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; } /** - * Profile mysql speed + * Get the possible connection key + * @param string|null $key + * @return string + */ + private static function getKey(?string $key = null): string + { + return (is_null($key)) ? "default" : $key; + } + + /** + * 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) { @@ -262,52 +338,4 @@ public static function endProfile($html = true): string|array 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 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..d7e0032 --- /dev/null +++ b/ConnectTest.php @@ -0,0 +1,42 @@ +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..2a737e2 100755 --- a/Create.php +++ b/Create.php @@ -67,6 +67,7 @@ namespace MaplePHP\Query; +use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Exceptions\QueryCreateException; class Create @@ -536,11 +537,12 @@ 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; } diff --git a/DB.php b/DB.php index 97714ef..11aa82f 100755 --- a/DB.php +++ b/DB.php @@ -4,11 +4,12 @@ namespace MaplePHP\Query; use MaplePHP\Query\Exceptions\ConnectException; +use MaplePHP\Query\Exceptions\DBQueryException; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\MigrateInterface; use MaplePHP\Query\Interfaces\DBInterface; use MaplePHP\Query\Exceptions\DBValidationException; -use MaplePHP\Query\Exceptions\DBQueryException; +use MaplePHP\Query\Exceptions\ResultException; //use MaplePHP\Query\Utility\Attr; use MaplePHP\Query\Utility\WhitelistMigration; @@ -36,7 +37,6 @@ class DB extends AbstractDB private $sql; private $dynamic; private ?string $returning = null; - protected ?string $pluck = null; /** * It is a semi-dynamic method builder that expects certain types of objects to be set @@ -44,7 +44,7 @@ class DB extends AbstractDB * @param array $args * @return self * @throws ConnectException - * @throws DBQueryException + * @throws ResultException */ public static function __callStatic(string $method, array $args) { @@ -53,8 +53,8 @@ public static function __callStatic(string $method, array $args) $table = array_pop($args); $inst = self::table($table); $inst->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 +83,7 @@ public static function __callStatic(string $method, array $args) } else { $inst = new self(); + //$inst->setConnKey(Connect::$current); } return $inst; @@ -93,8 +94,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 +105,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 +142,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 +153,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 +191,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 +210,7 @@ protected function select(): self * Select view * @return self * @throws DBValidationException + * @throws ConnectException */ protected function selectView(): self { @@ -218,6 +220,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 +232,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 +248,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 +264,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 @@ -546,7 +509,7 @@ 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( @@ -559,11 +522,11 @@ public function join( $this->join = array_merge($this->join, $this->buildJoinFromMig($table, $type)); } else { if (is_null($where)) { - throw new DBQueryException("You need to specify the 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"; @@ -606,7 +569,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 +581,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 +592,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 @@ -753,6 +719,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 +759,7 @@ private function buildWhere(string $prefix, ?array $where): string $out .= (($index > 0) ? " $firstAnd" : "") . " ("; $out .= $this->whereArrToStr($array); $out .= ")"; + $index++; } } @@ -822,15 +790,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 +812,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 +822,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..4caf4e2 --- /dev/null +++ b/DBTest.php @@ -0,0 +1,461 @@ +connection = $handler->execute(); + $this->prefix = $handler->getPrefix(); + } + + /** + * Used to make methods into dynamic shortcuts + * @param string $method + * @param array $args + * @return array|bool|object|string + * @throws ResultException + */ + public function __call(string $method, array $args): array|bool|object|string + { + $camelCaseArr = Helpers::extractCamelCase($method); + $shift = array_shift($camelCaseArr); + + 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); + $this->pluck = trim(end($pluck)); + $this->columns($args); + break; + case "where": + case "having": + Helpers::camelLoop($camelCaseArr, $args, function ($col, $val) use ($shift) { + $this->{$shift}($col, $val); + }); + break; + case "order": + if ($camelCaseArr[0] === "By") { + array_shift($camelCaseArr); + } + $ace = end($camelCaseArr); + foreach ($args as $val) { + $this->order($val, $ace); + } + break; + case "join": + $this->join($args[0], ($args[1] ?? null), ($args[2] ?? []), $camelCaseArr[0]); + break; + default: + return $this->query($this, $method, $args); + } + return $this; + } + + /** + * @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(); + } + + $table = Helpers::separateAlias($table); + $inst->alias = $table['alias']; + $inst->table = Helpers::getAttr($table['table'])->enclose(false); + if (is_null($inst->alias)) { + $inst->alias = $inst->table; + } + + return $inst; + } + + /** + * Select protected mysql columns + * @param string|array $columns + * @return self + */ + public function columns(string|array|AttrInterface ...$columns): self + { + $this->columns = $columns; + return $this; + } + + /** + * 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 + { + $this->compare = Helpers::operator($operator); + return $this; + } + + /** + * Chaining where with mysql "AND" or with "OR" + * @return self + */ + public function and(): self + { + $this->whereAnd = "AND"; + return $this; + } + + /** + * Chaining where with mysql "AND" or with "OR" + * @return self + */ + public function or(): self + { + $this->whereAnd = "OR"; + return $this; + } + + /** + * Chaining with where "NOT" ??? + * @return self + */ + public function not(): self + { + $this->whereNot = true; + return $this; + } + + + /** + * 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|null $operator Change comparison operator from default "=". + * @return self + */ + public function where(string|AttrInterface $key, string|int|float|AttrInterface $val, ?string $operator = null): self + { + // Whitelist operator + if (!is_null($operator)) { + $this->compare = Helpers::operator($operator); + } + $this->setWhereData($key, $val, $this->where); + return $this; + } + + /** + * Set Mysql ORDER + * @param string|AttrInterface $col Mysql Column + * @param string $sort Mysql sort type. Only "ASC" OR "DESC" is allowed, anything else will become "ASC". + * @return self + */ + public function order(string|AttrInterface $col, 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); + } + */ + $sort = Helpers::orderSort($sort); // Whitelist + $this->order[] = "$col $sort"; + return $this; + } + + /** + * Add a limit and maybe an offset + * @param int $limit + * @param int|null $offset + * @return self + */ + public function limit(int $limit, ?int $offset = null): self + { + $this->limit = $limit; + if (!is_null($offset)) { + $this->offset = $offset; + } + return $this; + } + + /** + * Add an offset (if limit is not set then it will automatically become "1"). + * @param int $offset + * @return self + */ + public function offset(int $offset): self + { + $this->offset = $offset; + return $this; + } + + + /** + * Add group + * @param array $columns + * @return self + */ + public function group(array ...$columns): self + { + /* + if (!is_null($this->migration) && !$this->migration->columns($columns)) { + throw new DBValidationException($this->migration->getMessage(), 1); + } + */ + $this->group = $columns; + return $this; + } + + /** + * Postgre specific function + * @param string $column + * @return $this + */ + public function returning(string $column): self + { + $this->returning = $column; + return $this; + } + + /** + * Mysql JOIN query (Default: INNER) + * @param string|array|MigrateInterface $table Mysql table name (if array e.g. [TABLE_NAME, ALIAS]) or MigrateInterface instance + * @param string|array|null $where Where data (as array or string e.g. string is raw) + * @param array $sprint Use sprint to prep data + * @param string $type Type of join + * @return self + * @throws ConnectException + * @throws ResultException + * @throws DBValidationException + */ + public function join( + string|array|MigrateInterface $table, + string|array $where = null, + array $sprint = array(), + string $type = "INNER" + ): self { + + if ($table instanceof MigrateInterface) { + $this->join = array_merge($this->join, $this->buildJoinFromMig($table, $type)); + } else { + if (is_null($where)) { + throw new ResultException("You need to specify the argument 2 (where) value!", 1); + } + + $prefix = $this->connInst()->getHandler()->getPrefix(); + $arr = Helpers::separateAlias($table); + $table = $arr['table']; + $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " $table"; + + $data = array(); + if (is_array($where)) { + foreach ($where as $key => $val) { + if (is_array($val)) { + foreach ($val as $grpKey => $grpVal) { + if(!($grpVal instanceof AttrInterface)) { + $grpVal = Helpers::withAttr($grpVal)->enclose(false); + } + $this->setWhereData($grpKey, $grpVal, $data); + } + } else { + if(!($val instanceof AttrInterface)) { + $val = Helpers::withAttr($val)->enclose(false); + } + $this->setWhereData($key, $val, $data); + } + } + //$out = $this->buildWhere("", $data); + } else { + //$out = $this->sprint($where, $sprint); + } + $type = Helpers::joinTypes(strtoupper($type)); // Whitelist + + $this->join[] = [ + "type" => $type, + "prefix" => $prefix, + "table" => $this->prefix . $table, + "alias" => $alias, + "where" => $where, + "whereData" => $data, + "sprint" => $sprint + ]; + $this->joinedTables[$table] = "$prefix$table"; + } + return $this; + } + + function buildSelect() { + + } + + /** + HELPERS + */ + + + /** + * 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 = array(); + $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)"; + } + } + } + } + + $this->joinedTables[$migTable] = $prefix . $migTable; + } + return $joinArr; + } + + /** + * 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|AttrInterface $key, string|int|float|AttrInterface $val, ?array &$data): void + { + if (is_null($data)) { + $data = array(); + } + /* + $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][] = [ + "not" => $this->whereNot, + "operator" => $this->compare, + "value" => $val + ]; + + $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 + { + if (!is_null($this->where)) { + $this->whereIndex++; + } + $this->resetWhere(); + $call($this); + $this->whereIndex++; + return $this; + } + + + /** + * Will reset Where input + * @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 protected function query(string|self $sql, ?string $method = null, array $args = []): array|object|bool|string + { + $query = new Query($sql, $this->connection); + $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; + } + +} \ No newline at end of file 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..6fca13a --- /dev/null +++ b/Handlers/MySQL/MySQLConnect.php @@ -0,0 +1,70 @@ +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 + */ + function query(string $query, int $result_mode = MYSQLI_STORE_RESULT): mysqli_result|bool + { + return parent::query($query, $result_mode); + } + + /** + * Returns the value generated for an AI column by the last query + * @param string|null $column Is only used with PostgreSQL! + * @return int + */ + function insert_id(?string $column = null):int + { + return $this->insert_id; + } + + /** + * Close connection + * @return bool + */ + function close(): true + { + return $this->close(); + } +} \ No newline at end of file diff --git a/Handlers/MySQLHandler.php b/Handlers/MySQLHandler.php index ee25f28..48ed775 100755 --- a/Handlers/MySQLHandler.php +++ b/Handlers/MySQLHandler.php @@ -5,6 +5,8 @@ use InvalidArgumentException; use MaplePHP\Query\Exceptions\ConnectException; +use MaplePHP\Query\Handlers\MySQL\MySQLConnect; +use MaplePHP\Query\Interfaces\ConnectInterface; use MaplePHP\Query\Interfaces\HandlerInterface; use mysqli; @@ -18,7 +20,7 @@ class MySQLHandler implements HandlerInterface private string $charset = "utf8mb4"; private int $port; private string $prefix = ""; - private ?mysqli $connection; + private ?mysqli $connection = null; public function __construct(string $server, string $user, string $pass, string $dbname, int $port = 3306) { @@ -39,7 +41,7 @@ public function getType(): string } /** - * Set MySqli charset + * Set MySql charset * @param string $charset */ public function setCharset(string $charset): void @@ -70,12 +72,12 @@ public function hasConnection(): bool /** * Connect to database - * @return mysqli + * @return ConnectInterface * @throws ConnectException */ - public function execute(): mysqli + public function execute(): ConnectInterface { - $this->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); } @@ -175,15 +177,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..9e2eea2 100755 --- a/Handlers/PostgreSQL/PostgreSQLConnect.php +++ b/Handlers/PostgreSQL/PostgreSQLConnect.php @@ -3,27 +3,44 @@ namespace MaplePHP\Query\Handlers\PostgreSQL; +use Exception; use MaplePHP\Query\Exceptions\ConnectException; +use MaplePHP\Query\Exceptions\ResultException; +use MaplePHP\Query\Interfaces\ConnectInterface; use PgSql\Connection; use PgSql\Result; -class PostgreSQLConnect +class PostgreSQLConnect implements ConnectInterface { - public $error; + public string $error = ""; private Connection $connection; private PostgreSQLResult|Result $query; + /** + * @param string $server + * @param string $user + * @param string $pass + * @param string $dbname + * @param int $port + * @throws ConnectException + */ public function __construct(string $server, string $user, string $pass, string $dbname, int $port = 5432) { if(!function_exists('pg_connect')) { throw new ConnectException('PostgreSQL php functions is missing and needs to be installed.', 1); } - $this->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); } + } public function getConnection(): Connection @@ -33,28 +50,29 @@ public function getConnection(): Connection /** * 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 + 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; } @@ -89,18 +107,23 @@ function rollback(): bool /** * Get insert ID * @return mixed + * @throws ResultException */ 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 + function close(): true { pg_close($this->connection); + return true; } } diff --git a/Handlers/PostgreSQLHandler.php b/Handlers/PostgreSQLHandler.php index 0718f24..7dc882f 100755 --- a/Handlers/PostgreSQLHandler.php +++ b/Handlers/PostgreSQLHandler.php @@ -5,10 +5,10 @@ use InvalidArgumentException; use MaplePHP\Query\Exceptions\ConnectException; +use MaplePHP\Query\Interfaces\ConnectInterface; use MaplePHP\Query\Interfaces\HandlerInterface; use MaplePHP\Query\Handlers\PostgreSQL\PostgreSQLConnect; use MaplePHP\Query\Handlers\PostgreSQL\PostgreSQLResult; -use PgSql\Connection; class PostgreSQLHandler implements HandlerInterface { @@ -20,7 +20,7 @@ class PostgreSQLHandler implements HandlerInterface private string $charset = "utf8"; private int $port; private string $prefix = ""; - private PostgreSQLConnect $connection; + private ?PostgreSQLConnect $connection = null; public function __construct(string $server, string $user, string $pass, string $dbname, int $port = 5432) { @@ -67,19 +67,18 @@ public function setPrefix(string $prefix): void */ public function hasConnection(): bool { - return ($this->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 +150,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 diff --git a/Handlers/SQLite/SQLiteConnect.php b/Handlers/SQLite/SQLiteConnect.php index 3c1f0f1..776ffb1 100644 --- a/Handlers/SQLite/SQLiteConnect.php +++ b/Handlers/SQLite/SQLiteConnect.php @@ -6,26 +6,26 @@ use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\ConnectInterface; 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,16 @@ 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(); - - //$this->query = parent::query($query); return false; } @@ -93,4 +92,13 @@ function insert_id(?string $column = null): int return $this->connection->lastInsertRowID(); } + /** + * Close connection + * @return bool + */ + function close(): true + { + return true; + } + } \ No newline at end of file diff --git a/Handlers/SQLite/SQLiteResult.php b/Handlers/SQLite/SQLiteResult.php index 3eacd81..172279a 100644 --- a/Handlers/SQLite/SQLiteResult.php +++ b/Handlers/SQLite/SQLiteResult.php @@ -3,7 +3,6 @@ namespace MaplePHP\Query\Handlers\SQLite; -use MaplePHP\DTO\DynamicDataAbstract; use ReflectionClass; use ReflectionException; use SQLite3; @@ -11,12 +10,10 @@ class SQLiteResult { - - public $index = -1; public int|string $num_rows = 0; - public array|bool $rows = false; - public array|bool $rowsObj = false; - + private int $index = -1; + private array|bool $rows = false; + private array|bool $rowsObj = false; private SQLite3 $connection; private SQLite3Result|false $query = false; @@ -182,7 +179,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/SQLiteHandler.php b/Handlers/SQLiteHandler.php index 5cb96a0..bc057e2 100755 --- a/Handlers/SQLiteHandler.php +++ b/Handlers/SQLiteHandler.php @@ -6,6 +6,7 @@ use Exception; use InvalidArgumentException; use MaplePHP\Query\Exceptions\ConnectException; +use MaplePHP\Query\Interfaces\ConnectInterface; use MaplePHP\Query\Interfaces\HandlerInterface; use MaplePHP\Query\Handlers\SQLite\SQLiteConnect; use SQLite3; @@ -17,7 +18,7 @@ class SQLiteHandler implements HandlerInterface private ?string $charSetName = null; private string $charset = "UTF-8"; private string $prefix = ""; - private ?SQLiteConnect $connection; + private ?SQLiteConnect $connection = null; public function __construct(string $database) { @@ -57,12 +58,14 @@ public function setPrefix(string $prefix): void /** * Check if a connections is open * @return bool + * @throws \ReflectionException */ public function hasConnection(): bool { if (!is_null($this->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 +75,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; } @@ -136,7 +139,7 @@ public function close(): void */ public function prep(string $value): string { - return SQLiteConnect::escapeString($value); + return SQLite3::escapeString($value); } /** @@ -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/ConnectInterface.php b/Interfaces/ConnectInterface.php index 3097eac..c4c0ba1 100644 --- a/Interfaces/ConnectInterface.php +++ b/Interfaces/ConnectInterface.php @@ -16,9 +16,10 @@ 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; /** @@ -47,4 +48,10 @@ function rollback(): bool; * @return int */ function insert_id(?string $column = null): int; + + /** + * Close connection + * @return bool + */ + function close(): true; } \ No newline at end of file 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/Utility/Attr.php b/Utility/Attr.php index 3dbe363..d737f90 100755 --- a/Utility/Attr.php +++ b/Utility/Attr.php @@ -3,9 +3,13 @@ namespace MaplePHP\Query\Utility; use MaplePHP\Query\Connect; +use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\DTO\Format\Encode; +/** + * MAKE IMMUTABLE in future + */ class Attr implements AttrInterface { private $value; @@ -13,14 +17,14 @@ class Attr implements AttrInterface private $hasBeenEncoded = false; private $prep = true; private $enclose = true; - private $jsonEncode = true; + private $jsonEncode = false; private $encode = false; /** * Initiate the instance - * @param array|string|int|float $value + * @param float|int|array|string $value */ - public function __construct($value) + public function __construct(float|int|array|string $value) { $this->value = $value; $this->raw = $value; @@ -33,13 +37,13 @@ public function __construct($value) */ public static function value(array|string|int|float $value): self { - $inst = new self($value); - return $inst; + return new self($value); } /** - * Process string after your choises + * Process string after your choices * @return string + * @throws ConnectException */ public function __toString(): string { @@ -47,22 +51,24 @@ public function __toString(): string } /** + * 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 diabled then quotes will be encoded + * If prepped is disabled then quotes will be encoded * @return string + * @throws ConnectException */ public function getValue(): string { 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)) { + + // Array values will automatically be json encoded + 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); @@ -73,7 +79,7 @@ public function getValue(): string } if ($this->enclose) { - $this->value = "'{$this->value}'"; + $this->value = "'$this->value'"; } } return $this->value; 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..c42e01f --- /dev/null +++ b/Utility/Helpers.php @@ -0,0 +1,170 @@ +", ">=", "<", "<>", "!=", "<=", "<=>"]; // 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"; + } + + /** + * 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 = array(); + 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]; + } + + +} \ No newline at end of file diff --git a/Utility/WhitelistMigration.php b/Utility/WhitelistMigration.php index a8ee6fe..0eb6e17 100755 --- a/Utility/WhitelistMigration.php +++ b/Utility/WhitelistMigration.php @@ -5,8 +5,6 @@ use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\MigrateInterface; -use MaplePHP\Query\Utility\Attr; -use MaplePHP\Query\Connect; class WhitelistMigration { @@ -24,13 +22,13 @@ class WhitelistMigration "BOOLEAN" ]; - private $mig; - private $data; - private $message; + private MigrateInterface $mig; + private array $data; + private ?string $message; /** * WhitelistMigration will take the migration files and use them to make a whitelist validation - * It kinda works like a custom built CSP filter but for the Database! + * It kinda works like a custom-built CSP filter but for the Database! * @param MigrateInterface $mig */ public function __construct(MigrateInterface $mig) @@ -69,7 +67,7 @@ public function getTable(): string /** * Get possible error message - * @return string + * @return string|null */ public function getMessage(): ?string { @@ -115,14 +113,16 @@ public function where(string $key, AttrInterface $value): bool $key = substr($key, $colPrefix + 1); } - // Key is assosiated with a Internal MySQL variable then return that + // Key is associated with an Internal MySQL variable then return that // value to check if the variable type is of the right type - if (Connect::hasVariable($key)) { + /* + if (Connect::hasVariable($key)) { $value = Connect::getVariableValue($key); } + */ if (!isset($this->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 +130,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/tests/database.sqlite b/tests/database.sqlite index aa5c1210f459f9c5a08777026bb8253e9d72ab5f..959534d3f47b9b14ac2322bbd34c957196ed4bb2 100644 GIT binary patch delta 1208 zcma)5Pi)&%9Da`f#LntGp{goblJOcO+MtFp%;12wk);7D(zH|wtUFNNVy}IgV<)!5 zmK|u{hLAWQC^_yzIdiEr32{IiCJm4f2e=>(`=jl|X(t4N1Hb2Zk&uRjt@o4t`Fr2* z`+c@|U*7#p`Xv9@3_?hSCBu?nW%m}XnHls<@VS}&4$0;|!c*oS=DxXq(65*~ZAF`% zwxp1GwsR?Bc9-^CXESobXtP5jXUh+UYHhtzst4&gR1hqsK;^$?) zBBy69OS)2x=sDMU1a%DU{Hv3abZS*kH|qb{0DNr<56lC9PC%NgEz)c^8jE9V08b#Y zRDY$~ZnVh!v-683k9M6Bd6x#AZJKP`fqfHFEJD`SmshIm8|3x+227JQE0-b4rF>rU z4J9X?JM*Asq8=WYl)NkO_0E`F9?FFRi08f6>zQ8ub7LJtuDZT47ts z-a%JKHwwK9Tzr5|@eRvZVle4I0>3;54O+CvgaI%ez&9Mky1y3AW(ul^2R7)V6UAH# zy1iK(7P+eH=omi%aKq_|ef~u+bUZsY|9$>K244I7V2D}9=x*_Hp660Js)$=-g?8X1 z-GNJ^h=SmQA};_vKJef8cl-eVhVSEB_!s;W{sG@S=zp8t+r77@%951Hh-w(3>bj`u zw5XaUs;Y{rD5A=;sHv1J8-@wm8oq`=`8NI$e~rJu*YMt0_xg^WR**EEwlDUv23K=h zE%|}T3s@C^IsuMjMF7eKcpl3FkSBnJQvyg$fakK3f{eMz{V@@ZhHs&VqiW+>$8P>w K5U&5vO#cAiolYSD delta 504 zcmXw$y>HYo6u@5sB%#D@3>0PzPfupkyzCc2k_i5>R}mYyux{=MIO&j)XfgS#)isTu&7I{q)~)dF#9 zuq58K^pWALfwT7M3jWq!Qa!`IS!IBpIblLW3=8Pjrr!S#(VU$KK`g?(VW6(ZMdWQ1U(qggoc8LT+ltd zE2v}%qiG?lsH5ET$cAOLP0%J4g~Smbv6O|AYtt>g`MM1&hlrrZT;{RhnZy}Q5*BIv zjr;oM2WU=kO)Jx~AyC%|3QCdlh=zHY=#&XBm}E4HQ^{m??XJ>q6oUve45fTg%Iw~VB kwO5xmWNBZQ_0W5U^shBephasConnection()) { $unit = new Unit(); + $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!"); - $unit->add("MySql Query builder", function ($inst) { - - $db = Connect::getInstance(); - $select = $db::select("id,a.name,b.name AS cat", ["test", "a"])->whereParent(0)->where("status", 0, ">")->limit(6); - $select->join(["test_category", "b"], "tid = id"); - - // 3 queries - $obj = $select->get(); - $arr = $select->fetch(); - $pluck = DB::table("test")->pluck("name")->get(); - - $inst->add($obj, [ - "isObject" => [], - "missingColumn" => function () use ($obj) { - return (isset($obj->name) && isset($obj->cat)); - } - ], "Data is missing"); - - $inst->add($arr, [ - "isArray" => [], - "noRows" => function () use ($arr) { - return (count($arr) > 0); - } - ], "Fetch feed empty"); - - $inst->add($pluck, [ - "isString" => [], - "length" => [1] - ], "Pluck is expected to return string"); - - $select = $db::select("id,test.name,test_category.name AS cat", new Test)->whereParent(0)->where("status", 0, ">")->limit(6); - $select->join(new TestCategory); - $obj = $select->obj(); - - $inst->add($obj, [ - "isObject" => [], - "missingColumn" => function () use ($obj) { - return (isset($obj->name) && isset($obj->cat)); - } - ], "Data is missing"); - }); - - /** - * This will test multiple databases AND - * validate sqLite database - */ - $unit->add("sqLite Query builder", function ($inst) { - - $sqLiteHandler = new SQLiteHandler(__DIR__ . "/database.sqlite"); - $sqLiteHandler->setPrefix("mp_"); - $connect = Connect::setHandler($sqLiteHandler, "lite"); - $connect->execute(); - - // Access sqLite connection - $select = Connect::getInstance("lite")::select("id,name,content", "test")->whereStatus(1)->limit(3); - $result = $select->fetch(); - $inst->add($result, [ - "isArray" => [], - "rows" => function () use ($result) { - return (count($result) === 3); - } - ], "Fetch should equal to 3"); - }); - - /** - * This will test multiple databases AND - * validate sqLite database - */ - $unit->add("sqLite Query builder", function ($inst) { - - $sqLiteHandler = new PostgreSQLHandler("127.0.0.1", "postgres", "", "maplephp"); - $sqLiteHandler->setPrefix("maple_"); - $connect = Connect::setHandler($sqLiteHandler, "psg"); - $connect->execute(); - - // Access sqLite connection - $select = Connect::getInstance("psg")::select("id,name", ["test", "a"])->limit(2); - $result = $select->fetch(); - $inst->add($result, [ - "isArray" => [], - "rows" => function () use ($result) { - return (count($result) === 2); - } - ], "Fetch should equal to 2"); - }); + 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"); + + + /* + $select->union($union); + + + + $arr = $select->fetch(); + + $inst->add(count($arr), [ + "equal" => [16], + ], "Union does not seem to match"); + */ + + + + $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(); + + + + + + + + + + + + + }); + } $unit->execute(); } From ebbb4a6e1e57a1179270a40f964ff8c6350f1878 Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Mon, 16 Sep 2024 18:53:27 +0200 Subject: [PATCH 2/3] Imporve structure --- DB.php | 26 +- DBTest.php | 492 +++++++++++++++------- Handlers/MySQL/MySQLConnect.php | 10 + Handlers/PostgreSQL/PostgreSQLConnect.php | 10 + Handlers/SQLite/SQLiteConnect.php | 10 + Handlers/SQLiteHandler.php | 1 - Interfaces/AttrInterface.php | 7 - Interfaces/ConnectInterface.php | 7 + Interfaces/DBInterface.php | 182 -------- Prepare.php | 35 ++ Query.php | 31 +- QueryBuilder.php | 273 ++++++++++++ Utility/Attr.php | 236 +++++++---- Utility/Helpers.php | 74 +++- tests/unitary-db.php | 86 ++-- 15 files changed, 1024 insertions(+), 456 deletions(-) create mode 100755 Prepare.php create mode 100755 QueryBuilder.php diff --git a/DB.php b/DB.php index 11aa82f..ff1f525 100755 --- a/DB.php +++ b/DB.php @@ -348,19 +348,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; } @@ -382,18 +382,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; } @@ -442,20 +442,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; } diff --git a/DBTest.php b/DBTest.php index 4caf4e2..d9acf07 100755 --- a/DBTest.php +++ b/DBTest.php @@ -3,9 +3,13 @@ namespace MaplePHP\Query; +use BadMethodCallException; +use InvalidArgumentException; use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Exceptions\DBValidationException; +use MaplePHP\Query\Interfaces\DBInterface; use MaplePHP\Query\Interfaces\HandlerInterface; +use MaplePHP\Query\Utility\Attr; use MaplePHP\Query\Utility\Helpers; use MaplePHP\Query\Exceptions\ResultException; use MaplePHP\Query\Interfaces\AttrInterface; @@ -16,36 +20,57 @@ /** * @method pluck(string $string) */ -class DBTest +class DBTest implements DBInterface { + private HandlerInterface $handler; private ConnectInterface $connection; private ?WhitelistMigration $migration = null; - - private string $prefix; - private string|AttrInterface $table; - private ?string $alias = null; - private ?string $pluck = null; - private string|array $columns; - private string|array $order; - private string $compare = "="; - private bool $whereNot = false; - private string $whereAnd = "AND"; - private int $whereIndex = 0; - private ?array $where; - private int $limit; - private int $offset; - private array $group; - private string $returning; - private array $joinedTables; + private AttrInterface $attr; + private ?QueryBuilder $builder = null; + + protected string $prefix; + protected string|AttrInterface $table; + protected ?AttrInterface $alias = null; + protected ?string $pluck = null; + protected ?array $columns = null; + protected bool $distinct = false; + protected ?array $order = null; + protected string $compare = "="; + protected bool $whereNot = false; + protected string $whereAnd = "AND"; + protected int $whereIndex = 0; + protected ?array $where = null; + protected ?array $having = null; + protected null|int|AttrInterface $limit = null; + protected ?int $offset = null; + protected ?array $group = null; + protected string $returning = ""; + protected array $join = []; + protected array $set = []; + protected $sql; + protected bool $prepare = false; + + protected bool $explain = false; + protected bool $noCache = false; + protected bool $calRows = false; + + protected ?array $union = null; /** * @throws ConnectException */ public function __construct(HandlerInterface $handler) { + $this->handler = $handler; $this->connection = $handler->execute(); $this->prefix = $handler->getPrefix(); + $this->attr = new Attr($this->connection); + } + + public function __toString(): string + { + return $this->sql(); } /** @@ -53,6 +78,8 @@ public function __construct(HandlerInterface $handler) * @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 @@ -60,6 +87,7 @@ 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] ?? ""); @@ -68,13 +96,13 @@ public function __call(string $method, array $args): array|bool|object|string } $pluck = explode(".", $args); - $this->pluck = trim(end($pluck)); - $this->columns($args); + $inst->pluck = trim(end($pluck)); + $inst = $inst->columns($args); break; case "where": case "having": - Helpers::camelLoop($camelCaseArr, $args, function ($col, $val) use ($shift) { - $this->{$shift}($col, $val); + Helpers::camelLoop($camelCaseArr, $args, function ($col, $val) use ($shift, &$inst) { + $inst = $inst->{$shift}($col, $val); }); break; case "order": @@ -83,16 +111,25 @@ public function __call(string $method, array $args): array|bool|object|string } $ace = end($camelCaseArr); foreach ($args as $val) { - $this->order($val, $ace); + $inst = $inst->order($val, $ace); } break; case "join": - $this->join($args[0], ($args[1] ?? null), ($args[2] ?? []), $camelCaseArr[0]); + $inst = $inst->join($args[0], ($args[1] ?? null), ($args[2] ?? []), $camelCaseArr[0]); break; default: - return $this->query($this, $method, $args); + return $inst->query($inst, $method, $args); } - return $this; + 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"); } /** @@ -103,30 +140,96 @@ public function table(string|array|MigrateInterface $table): self { $inst = clone $this; - if ($table instanceof MigrateInterface) { + /* + 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); - $table = Helpers::separateAlias($table); - $inst->alias = $table['alias']; - $inst->table = Helpers::getAttr($table['table'])->enclose(false); - if (is_null($inst->alias)) { - $inst->alias = $inst->table; - } 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 QueryBuilder + */ + public function getQueryBuilder(): QueryBuilder + { + if(is_null($this->builder)) { + 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 $columns + * + * @param string|AttrInterface ...$columns * @return self */ public function columns(string|array|AttrInterface ...$columns): self { - $this->columns = $columns; - return $this; + $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; } /** @@ -137,8 +240,9 @@ public function columns(string|array|AttrInterface ...$columns): self */ public function compare(string $operator): self { - $this->compare = Helpers::operator($operator); - return $this; + $inst = clone $this; + $inst->compare = Helpers::operator($operator); + return $inst; } /** @@ -147,8 +251,9 @@ public function compare(string $operator): self */ public function and(): self { - $this->whereAnd = "AND"; - return $this; + $inst = clone $this; + $inst->whereAnd = "AND"; + return $inst; } /** @@ -157,8 +262,9 @@ public function and(): self */ public function or(): self { - $this->whereAnd = "OR"; - return $this; + $inst = clone $this; + $inst->whereAnd = "OR"; + return $inst; } /** @@ -167,36 +273,55 @@ public function or(): self */ public function not(): self { - $this->whereNot = true; - return $this; + $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 $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 + */ + 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 where(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 { - // Whitelist operator + $inst = clone $this; if (!is_null($operator)) { - $this->compare = Helpers::operator($operator); + $inst->compare = Helpers::operator($operator); } - $this->setWhereData($key, $val, $this->where); - return $this; + $this->setWhereData($value, $column, $inst->having); + return $inst; } /** * 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 */ - public function order(string|AttrInterface $col, string $sort = "ASC"): self + public function order(string|AttrInterface $column, string $sort = "ASC"): self { // PREP AT BUILD //$col = $this->prep($col, false); @@ -205,9 +330,12 @@ public function order(string|AttrInterface $col, string $sort = "ASC"): self throw new DBValidationException($this->migration->getMessage(), 1); } */ - $sort = Helpers::orderSort($sort); // Whitelist - $this->order[] = "$col $sort"; - return $this; + $inst = clone $this; + $inst->order[] = [ + "column" => $this->attr($column, Attr::COLUMN_TYPE), + "sort" => Helpers::orderSort($sort) + ]; + return $inst; } /** @@ -216,13 +344,14 @@ public function order(string|AttrInterface $col, string $sort = "ASC"): self * @param int|null $offset * @return self */ - public function limit(int $limit, ?int $offset = null): self + public function limit(int|AttrInterface $limit, null|int|AttrInterface $offset = null): self { - $this->limit = $limit; + $inst = clone $this; + $inst->limit = $this->attr($limit, Attr::VALUE_TYPE_NUM); if (!is_null($offset)) { - $this->offset = $offset; + $inst->offset($offset); } - return $this; + return $inst; } /** @@ -230,10 +359,11 @@ public function limit(int $limit, ?int $offset = null): self * @param int $offset * @return self */ - public function offset(int $offset): self + public function offset(int|AttrInterface $offset): self { - $this->offset = $offset; - return $this; + $inst = clone $this; + $inst->offset = $this->attr($offset, Attr::VALUE_TYPE_NUM); + return $inst; } @@ -242,15 +372,27 @@ public function offset(int $offset): self * @param array $columns * @return self */ - public function group(array ...$columns): self + public function group(...$columns): self { /* if (!is_null($this->migration) && !$this->migration->columns($columns)) { throw new DBValidationException($this->migration->getMessage(), 1); } */ - $this->group = $columns; - return $this; + $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; } /** @@ -260,8 +402,9 @@ public function group(array ...$columns): self */ public function returning(string $column): self { - $this->returning = $column; - return $this; + $inst = clone $this; + $inst->returning = $column; + return $inst; } /** @@ -271,9 +414,6 @@ public function returning(string $column): self * @param array $sprint Use sprint to prep data * @param string $type Type of join * @return self - * @throws ConnectException - * @throws ResultException - * @throws DBValidationException */ public function join( string|array|MigrateInterface $table, @@ -282,111 +422,108 @@ public function join( string $type = "INNER" ): self { + $inst = clone $this; if ($table instanceof MigrateInterface) { - $this->join = array_merge($this->join, $this->buildJoinFromMig($table, $type)); + die("FIX"); + } else { - if (is_null($where)) { + + /* + * if (is_null($where)) { throw new ResultException("You need to specify the argument 2 (where) value!", 1); } + */ - $prefix = $this->connInst()->getHandler()->getPrefix(); - $arr = Helpers::separateAlias($table); - $table = $arr['table']; - $alias = (!is_null($arr['alias'])) ? " {$arr['alias']}" : " $table"; + // Try to move this to the start of the method + $tableInst = clone $inst; + $tableInst->alias = null; + $tableInst = $tableInst->table($table); $data = array(); if (is_array($where)) { foreach ($where as $key => $val) { if (is_array($val)) { foreach ($val as $grpKey => $grpVal) { - if(!($grpVal instanceof AttrInterface)) { - $grpVal = Helpers::withAttr($grpVal)->enclose(false); - } - $this->setWhereData($grpKey, $grpVal, $data); + $inst->setWhereData($this->attr($grpVal, Attr::COLUMN_TYPE), $grpKey, $data); } } else { - if(!($val instanceof AttrInterface)) { - $val = Helpers::withAttr($val)->enclose(false); - } - $this->setWhereData($key, $val, $data); + $inst->setWhereData($this->attr($val, Attr::COLUMN_TYPE), $key, $data); } } - //$out = $this->buildWhere("", $data); - } else { - //$out = $this->sprint($where, $sprint); } $type = Helpers::joinTypes(strtoupper($type)); // Whitelist - $this->join[] = [ + $inst->join[] = [ "type" => $type, - "prefix" => $prefix, - "table" => $this->prefix . $table, - "alias" => $alias, + "table" => $tableInst->table, + "alias" => $tableInst->alias, "where" => $where, "whereData" => $data, "sprint" => $sprint ]; - $this->joinedTables[$table] = "$prefix$table"; } - return $this; - } - - function buildSelect() { - + return $inst; } /** - HELPERS + * 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; - /** - * 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 + 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 { - $joinArr = array(); - $prefix = $this->connInst()->getHandler()->getPrefix(); - $main = $this->getMainFKData(); - $data = $mig->getData(); - $this->migration->mergeData($data); - $migTable = $mig->getTable(); + $inst = clone $this; + $inst->prepare = true; + return $inst; + } - 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)"; - } - } - } - } - $this->joinedTables[$migTable] = $prefix . $migTable; - } - return $joinArr; + public function sql(): string + { + $this->builder = new QueryBuilder($this); + $sql = $this->builder->sql(); + + return $sql; } - + + public function execute() + { + $sql = $this->sql(); + echo $sql; + die(); + + } + /** * 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|AttrInterface $key, string|int|float|AttrInterface $val, ?array &$data): void + final protected function setWhereData(string|int|float|AttrInterface $val, string|AttrInterface $key, ?array &$data): void { if (is_null($data)) { $data = array(); @@ -400,9 +537,10 @@ final protected function setWhereData(string|AttrInterface $key, string|int|floa */ $data[$this->whereIndex][$this->whereAnd][$key][] = [ + "column" => $this->attr($key, Attr::COLUMN_TYPE), "not" => $this->whereNot, "operator" => $this->compare, - "value" => $val + "value" => $this->attr($val, Attr::VALUE_TYPE) ]; $this->resetWhere(); @@ -416,18 +554,20 @@ final protected function setWhereData(string|AttrInterface $key, string|int|floa */ public function whereBind(callable $call): self { - if (!is_null($this->where)) { - $this->whereIndex++; + $inst = clone $this; + if (!is_null($inst->where)) { + $inst->whereIndex++; } - $this->resetWhere(); - $call($this); - $this->whereIndex++; - return $this; + $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 @@ -447,7 +587,7 @@ protected function resetWhere(): void */ final protected function query(string|self $sql, ?string $method = null, array $args = []): array|object|bool|string { - $query = new Query($sql, $this->connection); + $query = new Query($this->connection, $sql); $query->setPluck($this->pluck); if (!is_null($method)) { if (method_exists($query, $method)) { @@ -458,4 +598,66 @@ final protected function query(string|self $sql, ?string $method = null, array $ return $query; } -} \ No newline at end of file + + /** + 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 = array(); + $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 = 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; + } + +} diff --git a/Handlers/MySQL/MySQLConnect.php b/Handlers/MySQL/MySQLConnect.php index 6fca13a..be50ae5 100644 --- a/Handlers/MySQL/MySQLConnect.php +++ b/Handlers/MySQL/MySQLConnect.php @@ -67,4 +67,14 @@ function close(): true { return $this->close(); } + + /** + * Prep value / SQL escape string + * @param string $value + * @return string + */ + function prep(string $value): string + { + return $this->real_escape_string($value); + } } \ No newline at end of file diff --git a/Handlers/PostgreSQL/PostgreSQLConnect.php b/Handlers/PostgreSQL/PostgreSQLConnect.php index 9e2eea2..d7bb099 100755 --- a/Handlers/PostgreSQL/PostgreSQLConnect.php +++ b/Handlers/PostgreSQL/PostgreSQLConnect.php @@ -126,4 +126,14 @@ function close(): true pg_close($this->connection); return true; } + + /** + * Prep value / SQL escape string + * @param string $value + * @return string + */ + function prep(string $value): string + { + return pg_escape_string($this->connection, $value); + } } diff --git a/Handlers/SQLite/SQLiteConnect.php b/Handlers/SQLite/SQLiteConnect.php index 776ffb1..04a3394 100644 --- a/Handlers/SQLite/SQLiteConnect.php +++ b/Handlers/SQLite/SQLiteConnect.php @@ -101,4 +101,14 @@ function close(): true return true; } + /** + * Prep value / SQL escape string + * @param string $value + * @return string + */ + function prep(string $value): string + { + return SQLite3::escapeString($value); + } + } \ No newline at end of file diff --git a/Handlers/SQLiteHandler.php b/Handlers/SQLiteHandler.php index bc057e2..12cb604 100755 --- a/Handlers/SQLiteHandler.php +++ b/Handlers/SQLiteHandler.php @@ -129,7 +129,6 @@ public function query(string $sql): bool|SQLiteResult */ public function close(): void { - $this->connection->close(); } /** 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 c4c0ba1..fe1d2d7 100644 --- a/Interfaces/ConnectInterface.php +++ b/Interfaces/ConnectInterface.php @@ -54,4 +54,11 @@ function insert_id(?string $column = null): int; * @return bool */ function close(): true; + + /** + * Prep value / SQL escape string + * @param string $value + * @return string + */ + function prep(string $value): string; } \ No newline at end of file diff --git a/Interfaces/DBInterface.php b/Interfaces/DBInterface.php index 98bbc32..81aeb77 100755 --- a/Interfaces/DBInterface.php +++ b/Interfaces/DBInterface.php @@ -9,188 +9,6 @@ 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 diff --git a/Prepare.php b/Prepare.php new file mode 100755 index 0000000..a59be09 --- /dev/null +++ b/Prepare.php @@ -0,0 +1,35 @@ +prepExecute(); + if(!method_exists($query, $name)) { + throw new \BadMethodCallException("The method '$name' does not exist in " . get_class($query) . "."); + } + return $query->$name(...$arguments); + } + + public function query(DBInterface $db): void + { + $this->query = $db; + $this->statements[] = $db->prepare(); + } + + private function prepExecute(): ?Query + { + $this->query->sql(); // Will build the query + return $this->query->bind($this->statements); + } + +} \ No newline at end of file diff --git a/Query.php b/Query.php index 603e5af..a5e90d7 100755 --- a/Query.php +++ b/Query.php @@ -4,21 +4,25 @@ namespace MaplePHP\Query; use MaplePHP\Query\Exceptions\ConnectException; +use MaplePHP\Query\Interfaces\ConnectInterface; use MaplePHP\Query\Interfaces\DBInterface; class Query { private $sql; + private $prepare; + private ?array $bind = null; private ?string $pluck = null; - private ?Connect $connection = null; + private ConnectInterface $connection; - public function __construct(string|DBInterface $sql, $connection = null) + public function __construct(ConnectInterface $connection, string|DBInterface $sql) { $this->sql = $sql; if ($sql instanceof DBInterface) { + $this->prepare = $this->sql->__get("prepare"); $this->sql = $sql->sql(); } - $this->connection = is_null($connection) ? Connect::getInstance() : $connection; + $this->connection = $connection; } public function setPluck(?string $pluck): void @@ -26,6 +30,12 @@ public function setPluck(?string $pluck): void $this->pluck = $pluck; } + public function bind(array $set): self + { + $this->bind = $set; + return $this; + } + /** * Execute query result * @return object|array|bool @@ -33,6 +43,21 @@ public function setPluck(?string $pluck): void */ public function execute(): object|array|bool { + if(!is_null($this->bind)) { + $arr = []; + $stmt = $this->connection->prepare($this->bind[0]->sql()); + foreach($this->bind as $dbInst) { + $dbInst->sql(); + $ref = $dbInst->getQueryBuilder()->getSet(); + $stmt->bind_param(str_pad("", count($ref), "s"), ...$ref); + $stmt->execute(); + $arr[] = $stmt; + } + //$stmt->close(); + + return $arr; + } + if ($result = $this->connection->query($this->sql)) { return $result; } else { diff --git a/QueryBuilder.php b/QueryBuilder.php new file mode 100755 index 0000000..67b3c89 --- /dev/null +++ b/QueryBuilder.php @@ -0,0 +1,273 @@ +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')); + } + + public function sql(): string + { + + return $this->select(); + /* + if(is_null($this->sql)) { + $sql = $this->buildSelect(); + $set = $this->set; + + if($this->prepare) { + $set = array_pad([], count($this->set), "?"); + + } + $this->sql = vsprintf($sql, $set); + } + //array_pad([], count($whereSet), "?"); + //$rawSql = vsprintf($rawSql, $this->set); + return $this->sql; + */ + } + + /** + * 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, " "); + } + + + 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 ""; + } + + 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; + } + + 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 d737f90..717b542 100755 --- a/Utility/Attr.php +++ b/Utility/Attr.php @@ -2,48 +2,49 @@ namespace MaplePHP\Query\Utility; -use MaplePHP\Query\Connect; -use MaplePHP\Query\Exceptions\ConnectException; +use BadMethodCallException; +use InvalidArgumentException; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\DTO\Format\Encode; +use MaplePHP\Query\Interfaces\ConnectInterface; +use MaplePHP\Query\Interfaces\HandlerInterface; /** * MAKE IMMUTABLE in future */ class Attr implements AttrInterface { - private $value; - private $raw; - private $hasBeenEncoded = false; - private $prep = true; - private $enclose = true; - private $jsonEncode = false; - private $encode = false; + const RAW_TYPE = 0; + const VALUE_TYPE = 1; + const COLUMN_TYPE = 2; + const VALUE_TYPE_NUM = 3; + const VALUE_TYPE_STR = 4; - /** - * Initiate the instance - * @param float|int|array|string $value - */ - public function __construct(float|int|array|string $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) { - return new self($value); + $this->connection = $connection; } /** * Process string after your choices * @return string - * @throws ConnectException */ public function __toString(): string { @@ -51,90 +52,171 @@ public function __toString(): string } /** - * 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 - * @throws ConnectException + * 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 + { + $inst = new self($this->connection); + $inst->value = $value; + $inst->raw = $value; + return $inst; + } + + public function type(int $dataType): static { - if (!$this->hasBeenEncoded) { - $this->hasBeenEncoded = true; - $this->value = Encode::value($this->value) - ->specialChar($this->encode, ($this->prep ? ENT_NOQUOTES : ENT_QUOTES)) - ->urlEncode(false) - ->encode(); - - // Array values will automatically be json encoded - 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); - } - - if ($this->enclose) { - $this->value = "'$this->value'"; - } + $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)"!'); } - return $this->value; + $inst->type = $dataType; + if($dataType === self::RAW_TYPE) { + $inst = $inst->prep(false)->enclose(false)->encode(false)->sanitize(false); + } + + 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; } + public function isType(int $type): int + { + return ($this->type === $type); + } + + /** - * Get raw data from instance - * @return string|array + * IMMUTABLE: Enable/disable MySQL prep + * @param bool $prep + * @return static */ - public function getRaw(): string|array + public function prep(bool $prep): static { - return $this->raw; + $inst = clone $this; + $inst->prep = $prep; + return $inst; } /** - * Enable/disable MySQL prep - * @param bool $prep - * @return self + * Sanitize column types + * @param bool $sanitize + * @return $this */ - public function prep(bool $prep): self + public function sanitize(bool $sanitize): static { - $this->prep = $prep; - return $this; + $inst = clone $this; + $inst->sanitize = $sanitize; + return $inst; } /** - * Enable/disable string enclose + * 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): self + public function encode(bool $encode): static { - $this->encode = $encode; - return $this; + $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 getValue(): string + { + + $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) { + $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/Helpers.php b/Utility/Helpers.php index c42e01f..dade171 100644 --- a/Utility/Helpers.php +++ b/Utility/Helpers.php @@ -4,8 +4,8 @@ use BadMethodCallException; use InvalidArgumentException; -use MaplePHP\Query\Exceptions\DBValidationException; use MaplePHP\Query\Interfaces\AttrInterface; +use MaplePHP\Query\Interfaces\DBInterface; class Helpers { @@ -56,6 +56,50 @@ public static function joinTypes(string $val): string 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 = array(); + 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; + } + + function validateIdentifiers($column): bool + { + return (preg_match('/^[a-zA-Z0-9_]+$/', $column) !== false); + } + /** * Mysql Prep/protect string * @param mixed $val @@ -167,4 +211,32 @@ public static function separateAlias(string|array $data): array } + /** + * 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; + } + } \ No newline at end of file diff --git a/tests/unitary-db.php b/tests/unitary-db.php index 6a87194..209c566 100755 --- a/tests/unitary-db.php +++ b/tests/unitary-db.php @@ -2,8 +2,12 @@ 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; @@ -12,6 +16,60 @@ 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); + $unit->addTitle("Testing MaplePHP Query library!"); + + $unit->add("OK", function ($inst) use ($unit, $db) { + + $prepare = new Prepare(); + for($i = 1; $i < 6; $i++) { + + // + + /* + $test = $db->table("test") + ->columns("id", "name") + ->where("id", 1); + + + $test2 = $db->table("test") + ->columns("id", "name") + ->where("id", 3) + ->order("id", "DESC") + ->limit(20); + */ + + $inst = $db->table(["test", "a"]) + ->join(["test_category", "cat"], ["cat.tid" => "a.id"]) + ->columns("a.id", "a.name", ['cat.name' => "test"]) + ->where("id", $i); + + $prepare->query($inst); + + /* + print_r($inst->fetch()); + echo "\n\n"; + //print_r($test->sql()); + die; + */ + } + + print_r($prepare->execute()); + die; + + + }); + + $unit->execute(); +} + + +/* + $instances = [null, "postgresql", "sqlite"]; $sqLiteHandler = new PostgreSQLHandler("127.0.0.1", "postgres", "", "postgres"); @@ -69,19 +127,6 @@ $select = $db::select("cat_id AS id,name", "test_category"); - /* - $select->union($union); - - - - $arr = $select->fetch(); - - $inst->add(count($arr), [ - "equal" => [16], - ], "Union does not seem to match"); - */ - - $insert = $db::insert("test")->set([ "name" => "Test row", @@ -97,19 +142,6 @@ //print_r($db->connection()); //$insert->execute(); - - - - - - - - - - - }); } - - $unit->execute(); -} + */ \ No newline at end of file From 700c1a98698f91a2a9e55579a8ec09c16715ad5d Mon Sep 17 00:00:00 2001 From: Daniel Ronkainen Date: Thu, 10 Oct 2024 19:03:41 +0200 Subject: [PATCH 3/3] Add prepare container --- AbstractDB.php | 17 +- AbstractMigrate.php | 1 + Connect.php | 36 ++- ConnectTest.php | 5 +- Create.php | 36 +-- DB.php | 27 ++- DBTest.php | 108 +++++---- Handlers/MySQL/MySQLConnect.php | 24 +- Handlers/MySQLHandler.php | 3 +- Handlers/PostgreSQL/PostgreSQLConnect.php | 46 +++- Handlers/PostgreSQL/PostgreSQLResult.php | 10 +- Handlers/PostgreSQL/PostgreSQLStmt.php | 78 ++++++ Handlers/PostgreSQLHandler.php | 3 +- Handlers/SQLite/SQLiteConnect.php | 29 ++- Handlers/SQLite/SQLiteResult.php | 15 +- Handlers/SQLite/SQLiteStmt.php | 83 +++++++ Handlers/SQLiteHandler.php | 5 +- Interfaces/ConnectInterface.php | 15 +- Interfaces/DBInterface.php | 7 +- Interfaces/QueryBuilderInterface.php | 12 + Interfaces/ResultInterface.php | 5 +- Interfaces/StmtInterface.php | 38 +++ Prepare.php | 94 +++++++- Query.php | 80 +++++-- QueryBuilder.php | 80 +++---- QueryBuilderLegacy.php | 274 ++++++++++++++++++++++ Utility/Attr.php | 29 ++- Utility/Helpers.php | 13 +- Utility/WhitelistMigration.php | 1 + composer.json | 2 +- tests/unitary-database.php | 130 ++++++++++ tests/unitary-db.php | 48 +--- 32 files changed, 1068 insertions(+), 286 deletions(-) create mode 100755 Handlers/PostgreSQL/PostgreSQLStmt.php create mode 100755 Handlers/SQLite/SQLiteStmt.php create mode 100755 Interfaces/QueryBuilderInterface.php create mode 100644 Interfaces/StmtInterface.php create mode 100755 QueryBuilderLegacy.php create mode 100755 tests/unitary-database.php diff --git a/AbstractDB.php b/AbstractDB.php index 7989c67..04b62bf 100755 --- a/AbstractDB.php +++ b/AbstractDB.php @@ -1,4 +1,5 @@ prep($key, false); $val = $this->prep($val); @@ -278,7 +279,7 @@ final protected function whereArrToStr(array $array): string $out .= "$col {$row['operator']} {$row['value']} "; $count++; } - + } else { $out .= ($count) > 0 ? "$key $a " : $a; $count++; @@ -287,7 +288,7 @@ final protected function whereArrToStr(array $array): string } return $out; } - + /** * Get the Main FK data protocol * @return array @@ -295,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) { @@ -331,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); @@ -345,7 +346,7 @@ final protected function prepArr(array $arr, bool $enclose = true): array * @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)); } @@ -386,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(); @@ -452,4 +453,4 @@ final protected function query(string|self $sql, ?string $method = null, array $ } 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; } @@ -223,7 +213,7 @@ public function query(string $query, int $result_mode = 0): object|array|bool * Begin transaction * @return bool */ - function begin_transaction(): bool + public function begin_transaction(): bool { return $this->connection->begin_transaction(); } @@ -232,7 +222,7 @@ function begin_transaction(): bool * Commit transaction * @return bool */ - function commit(): bool + public function commit(): bool { return $this->connection->commit(); } @@ -241,7 +231,7 @@ function commit(): bool * Rollback transaction * @return bool */ - function rollback(): bool + public function rollback(): bool { return $this->connection->rollback(); } @@ -251,7 +241,7 @@ 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->insert_id($column); } @@ -260,7 +250,7 @@ function insert_id(?string $column = null): int * Close connection * @return bool */ - function close(): true + public function close(): true { return $this->connection->close(); } @@ -323,10 +313,12 @@ public function endProfile($html = true): string|array $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); @@ -335,7 +327,7 @@ public function endProfile($html = true): string|array $output .= "

"; return $output; } else { - return array("row" => $output, "total" => $total); + return ["row" => $output, "total" => $total]; } } } diff --git a/ConnectTest.php b/ConnectTest.php index d7e0032..38f2d7f 100755 --- a/ConnectTest.php +++ b/ConnectTest.php @@ -1,4 +1,5 @@ handler = $handler; diff --git a/Create.php b/Create.php index 2a737e2..e013cf4 100755 --- a/Create.php +++ b/Create.php @@ -74,7 +74,7 @@ class Create { private $sql; private $add; - private $addArr = array(); + private $addArr = []; private $prefix; private $type; private $args; @@ -88,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; @@ -249,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); @@ -293,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; @@ -548,7 +548,7 @@ public function execute() public function mysqlCleanArr(array $arr) { - $new = array(); + $new = []; foreach ($arr as $a) { $new[] = Connect::getInstance()->prep($a); } @@ -562,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) { @@ -818,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)); @@ -919,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; @@ -956,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 ff1f525..fdf0d89 100755 --- a/DB.php +++ b/DB.php @@ -1,4 +1,5 @@ $val) { if (is_array($val)) { foreach ($val as $grpKey => $grpVal) { @@ -642,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); @@ -666,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; @@ -708,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"; } @@ -880,4 +881,4 @@ 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 index d9acf07..131103c 100755 --- a/DBTest.php +++ b/DBTest.php @@ -1,4 +1,5 @@ attr = new Attr($this->connection); } + public function getConnection() + { + return $this->connection; + } + public function __toString(): string { return $this->sql(); @@ -185,12 +191,13 @@ public function attr(mixed $value, int $type): array|AttrInterface /** * When SQL query has been triggered then the QueryBuilder should exist - * @return QueryBuilder + * @return QueryBuilderInterface */ - public function getQueryBuilder(): QueryBuilder + public function getQueryBuilder(): QueryBuilderInterface { if(is_null($this->builder)) { - throw new BadMethodCallException("The query builder can only be called after query has been built."); + $this->sql(); + //throw new BadMethodCallException("The query builder can only be called after query has been built."); } return $this->builder; } @@ -198,7 +205,7 @@ public function getQueryBuilder(): QueryBuilder /** * Select protected mysql columns * - * @param string|AttrInterface ...$columns + * @param string|array|AttrInterface ...$columns * @return self */ public function columns(string|array|AttrInterface ...$columns): self @@ -311,7 +318,7 @@ public function having(string|AttrInterface $column, string|int|float|AttrInterf if (!is_null($operator)) { $inst->compare = Helpers::operator($operator); } - $this->setWhereData($value, $column, $inst->having); + $this->setWhereData($value, $column, $inst->having); return $inst; } @@ -340,9 +347,9 @@ public function order(string|AttrInterface $column, string $sort = "ASC"): self /** * Add a limit and maybe an offset - * @param int $limit - * @param int|null $offset - * @return self + * @param int|AttrInterface $limit + * @param int|AttrInterface|null $offset + * @return $this */ public function limit(int|AttrInterface $limit, null|int|AttrInterface $offset = null): self { @@ -356,8 +363,8 @@ public function limit(int|AttrInterface $limit, null|int|AttrInterface $offset = /** * Add an offset (if limit is not set then it will automatically become "1"). - * @param int $offset - * @return self + * @param int|AttrInterface $offset + * @return $this */ public function offset(int|AttrInterface $offset): self { @@ -418,7 +425,7 @@ public function returning(string $column): self public function join( string|array|MigrateInterface $table, string|array $where = null, - array $sprint = array(), + array $sprint = [], string $type = "INNER" ): self { @@ -439,7 +446,7 @@ public function join( $tableInst->alias = null; $tableInst = $tableInst->table($table); - $data = array(); + $data = []; if (is_array($where)) { foreach ($where as $key => $val) { if (is_array($val)) { @@ -478,7 +485,7 @@ public function union(DBInterface|string $dbInst, bool $allowDuplicate = false): $inst = clone $this; - if(!is_null($inst->order)) { + if(!is_null($inst->order)) { throw new \RuntimeException("You need to move your ORDER BY to the last UNION statement!"); } @@ -496,8 +503,8 @@ public function union(DBInterface|string $dbInst, bool $allowDuplicate = false): public function prepare(): self { $inst = clone $this; - $inst->prepare = true; - return $inst; + $this->prepare = true; + return $this; } @@ -505,18 +512,9 @@ public function sql(): string { $this->builder = new QueryBuilder($this); $sql = $this->builder->sql(); - return $sql; } - public function execute() - { - $sql = $this->sql(); - echo $sql; - die(); - - } - /** * Propagate where data structure * @param string|AttrInterface $key @@ -526,7 +524,7 @@ public function execute() final protected function setWhereData(string|int|float|AttrInterface $val, string|AttrInterface $key, ?array &$data): void { if (is_null($data)) { - $data = array(); + $data = []; } /* $key = (string)$this->prep($key, false); @@ -577,6 +575,7 @@ protected function resetWhere(): void $this->compare = "="; } + /** * Query result * @param string|self $sql @@ -585,7 +584,7 @@ protected function resetWhere(): void * @return array|object|bool|string * @throws ResultException */ - final protected function query(string|self $sql, ?string $method = null, array $args = []): array|object|bool|string + 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); @@ -598,6 +597,19 @@ final protected function query(string|self $sql, ?string $method = null, array $ 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 @@ -612,7 +624,7 @@ final protected function query(string|self $sql, ?string $method = null, array $ */ final protected function buildJoinFromMig(MigrateInterface $mig, string $type): array { - $joinArr = array(); + $joinArr = []; $prefix = $this->connInst()->getHandler()->getPrefix(); $main = $this->getMainFKData(); $data = $mig->getData(); @@ -648,7 +660,7 @@ final protected function buildJoinFromMig(MigrateInterface $mig, string $type): 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) { diff --git a/Handlers/MySQL/MySQLConnect.php b/Handlers/MySQL/MySQLConnect.php index be50ae5..5ab5632 100644 --- a/Handlers/MySQL/MySQLConnect.php +++ b/Handlers/MySQL/MySQLConnect.php @@ -7,6 +7,7 @@ use MaplePHP\Query\Interfaces\ConnectInterface; use mysqli; use mysqli_result; +use mysqli_stmt; class MySQLConnect extends mysqli implements ConnectInterface { @@ -18,9 +19,11 @@ class MySQLConnect extends mysqli implements ConnectInterface */ public function __construct(...$args) { + try { parent::__construct(...$args); } catch (Exception $e) { + // Make sure ConnectException will be called by exception chaining to it. // Make errors consistent through all the handlers throw new ConnectException('Failed to connect to MySQL: ' . $e->getMessage(), $e->getCode(), $e); } @@ -44,17 +47,28 @@ public function __call(string $method, array $arguments): object|false * @param int $result_mode * @return mysqli_result|bool */ - function query(string $query, int $result_mode = MYSQLI_STORE_RESULT): 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 */ - function insert_id(?string $column = null):int + public function insert_id(?string $column = null): int { return $this->insert_id; } @@ -63,7 +77,7 @@ function insert_id(?string $column = null):int * Close connection * @return bool */ - function close(): true + public function close(): true { return $this->close(); } @@ -73,8 +87,8 @@ function close(): true * @param string $value * @return string */ - function prep(string $value): string + public function prep(string $value): string { return $this->real_escape_string($value); } -} \ No newline at end of file +} diff --git a/Handlers/MySQLHandler.php b/Handlers/MySQLHandler.php index 48ed775..dc89217 100755 --- a/Handlers/MySQLHandler.php +++ b/Handlers/MySQLHandler.php @@ -1,4 +1,5 @@ connection; if (mysqli_multi_query($db, $sql)) { do { diff --git a/Handlers/PostgreSQL/PostgreSQLConnect.php b/Handlers/PostgreSQL/PostgreSQLConnect.php index d7bb099..9e1489c 100755 --- a/Handlers/PostgreSQL/PostgreSQLConnect.php +++ b/Handlers/PostgreSQL/PostgreSQLConnect.php @@ -1,4 +1,5 @@ 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 $method @@ -65,7 +93,7 @@ public function __call(string $method, array $arguments): Connection|false * @param int $result_mode * @return PostgreSQLResult|bool */ - function query($query, int $result_mode = 0): PostgreSQLResult|bool + public function query($query, int $result_mode = 0): PostgreSQLResult|bool { if($this->connection instanceof Connection) { $this->query = new PostgreSQLResult($this->connection); @@ -81,7 +109,7 @@ function query($query, int $result_mode = 0): PostgreSQLResult|bool * Begin transaction * @return bool */ - function begin_transaction(): bool + public function begin_transaction(): bool { return (bool)$this->query("BEGIN"); } @@ -90,7 +118,7 @@ function begin_transaction(): bool * Commit transaction * @return bool */ - function commit(): bool + public function commit(): bool { return (bool)$this->query("COMMIT"); } @@ -99,7 +127,7 @@ function commit(): bool * Rollback transaction * @return bool */ - function rollback(): bool + public function rollback(): bool { return (bool)$this->query("ROLLBACK"); } @@ -109,7 +137,7 @@ function rollback(): bool * @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."); @@ -121,7 +149,7 @@ function insert_id(?string $column = null): int * Close the connection * @return true */ - function close(): true + public function close(): true { pg_close($this->connection); return true; @@ -132,7 +160,7 @@ function close(): true * @param string $value * @return string */ - function prep(string $value): 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 7dc882f..c8056a0 100755 --- a/Handlers/PostgreSQLHandler.php +++ b/Handlers/PostgreSQLHandler.php @@ -1,4 +1,5 @@ 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 04a3394..79eacb4 100644 --- a/Handlers/SQLite/SQLiteConnect.php +++ b/Handlers/SQLite/SQLiteConnect.php @@ -5,11 +5,11 @@ use Exception; use MaplePHP\Query\Exceptions\ConnectException; use MaplePHP\Query\Interfaces\ConnectInterface; +use MaplePHP\Query\Interfaces\StmtInterface; use SQLite3; class SQLiteConnect implements ConnectInterface { - public string $error = ""; private SQLiteResult $query; @@ -55,11 +55,24 @@ public function query(string $query, int $result_mode = 0): SQLiteResult|false return false; } + /** + * 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; + } + /** * Begin transaction * @return bool */ - function begin_transaction(): bool + public function begin_transaction(): bool { return (bool)$this->query("BEGIN TRANSACTION"); } @@ -68,7 +81,7 @@ function begin_transaction(): bool * Commit transaction * @return bool */ - function commit(): bool + public function commit(): bool { return (bool)$this->query("COMMIT"); } @@ -77,7 +90,7 @@ function commit(): bool * Rollback transaction * @return bool */ - function rollback(): bool + public function rollback(): bool { return (bool)$this->query("ROLLBACK"); } @@ -87,7 +100,7 @@ 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(); } @@ -96,7 +109,7 @@ function insert_id(?string $column = null): int * Close connection * @return bool */ - function close(): true + public function close(): true { return true; } @@ -106,9 +119,9 @@ function close(): true * @param string $value * @return string */ - function prep(string $value): string + public function prep(string $value): string { return SQLite3::escapeString($value); } -} \ No newline at end of file +} diff --git a/Handlers/SQLite/SQLiteResult.php b/Handlers/SQLite/SQLiteResult.php index 172279a..b165a74 100644 --- a/Handlers/SQLite/SQLiteResult.php +++ b/Handlers/SQLite/SQLiteResult.php @@ -1,25 +1,31 @@ connection = $connection; + $this->query = $query; + if($this->query !== false) { + $this->preFetchData(); + } } /** @@ -53,7 +59,6 @@ public function fetch_object(string $class = "stdClass", array $constructor_args $data = $this->bindToClass($data, $class, $constructor_args); } $this->endIndex(); - return $data; } @@ -134,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; 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 12cb604..de27401 100755 --- a/Handlers/SQLiteHandler.php +++ b/Handlers/SQLiteHandler.php @@ -1,4 +1,5 @@ connection = new SQLiteConnect($this->database); - + } catch (Exception $e) { throw new ConnectException('Failed to connect to SQLite: ' . $e->getMessage(), $e->getCode(), $e); } @@ -150,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) { diff --git a/Interfaces/ConnectInterface.php b/Interfaces/ConnectInterface.php index fe1d2d7..ddf27d8 100644 --- a/Interfaces/ConnectInterface.php +++ b/Interfaces/ConnectInterface.php @@ -4,7 +4,6 @@ interface ConnectInterface { - /** * Access the database main class * @param string $method @@ -26,20 +25,20 @@ 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; /** @@ -47,18 +46,18 @@ 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; /** * Close connection * @return bool */ - function close(): true; + public function close(): true; /** * Prep value / SQL escape string * @param string $value * @return string */ - function prep(string $value): string; -} \ No newline at end of file + public function prep(string $value): string; +} diff --git a/Interfaces/DBInterface.php b/Interfaces/DBInterface.php index 81aeb77..b15b647 100755 --- a/Interfaces/DBInterface.php +++ b/Interfaces/DBInterface.php @@ -7,12 +7,15 @@ namespace MaplePHP\Query\Interfaces; +/** + * @method bind(\MaplePHP\Query\Prepare $param, array $statements) + */ interface DBInterface { - /** - * Genrate SQL string of current instance/query + * Generate SQL string of current instance/query * @return string */ public function sql(): string; + } 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(); @@ -20,16 +41,75 @@ public function __call(string $name, array $arguments): mixed return $query->$name(...$arguments); } - public function query(DBInterface $db): void + /** + * @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->query = $db; $this->statements[] = $db->prepare(); } - private function prepExecute(): ?Query + /** + * 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 { - $this->query->sql(); // Will build the query - return $this->query->bind($this->statements); + $query = $this->prepExecute(); + return $query->execute(); } -} \ No newline at end of file +} diff --git a/Query.php b/Query.php index a5e90d7..ed23513 100755 --- a/Query.php +++ b/Query.php @@ -1,16 +1,19 @@ sql = $sql; if ($sql instanceof DBInterface) { - $this->prepare = $this->sql->__get("prepare"); $this->sql = $sql->sql(); } $this->connection = $connection; @@ -30,8 +32,9 @@ public function setPluck(?string $pluck): void $this->pluck = $pluck; } - public function bind(array $set): self + public function bind($stmt, array $set): self { + $this->stmt = $stmt; $this->bind = $set; return $this; } @@ -44,18 +47,7 @@ public function bind(array $set): self public function execute(): object|array|bool { if(!is_null($this->bind)) { - $arr = []; - $stmt = $this->connection->prepare($this->bind[0]->sql()); - foreach($this->bind as $dbInst) { - $dbInst->sql(); - $ref = $dbInst->getQueryBuilder()->getSet(); - $stmt->bind_param(str_pad("", count($ref), "s"), ...$ref); - $stmt->execute(); - $arr[] = $stmt; - } - //$stmt->close(); - - return $arr; + return $this->executePrepare(); } if ($result = $this->connection->query($this->sql)) { @@ -65,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 @@ -76,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 */ @@ -95,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)) { @@ -118,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 index 67b3c89..15d3c10 100755 --- a/QueryBuilder.php +++ b/QueryBuilder.php @@ -3,7 +3,7 @@ * Query SQL string Builder * * This class will access the protected objects as "readonly" from the DB class. - * But as the readonly property only is available in 8.1, and I want this class + * But as the readonly property only is available in 8.1, and I want this class * to be supported as for PHP 8.0+, This will be the solution for a couple of years */ @@ -14,10 +14,11 @@ use http\Exception\RuntimeException; use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\DBInterface; +use MaplePHP\Query\Interfaces\QueryBuilderInterface; use MaplePHP\Query\Utility\Helpers; use MaplePHP\Query\Utility\Attr; -class QueryBuilder +class QueryBuilder implements QueryBuilderInterface { private DBInterface $db; private array $set = []; @@ -31,7 +32,7 @@ public function __toString(): string { return $this->sql(); } - + public function select(): string { $explain = $this->getExplain(); @@ -39,8 +40,8 @@ public function select(): string $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')); + $where = $this->getWhere("WHERE", $this->db->where); + $having = $this->getWhere("HAVING", $this->db->having); $order = $this->getOrder(); $limit = $this->getLimit(); $group = $this->getGroup(); @@ -52,28 +53,16 @@ public function select(): string public function getTable(): string { - return Helpers::addAlias($this->db->__get('table'), $this->db->__get('alias')); + return Helpers::addAlias($this->db->table, $this->db->alias); } + /** + * Get sql code + * @return string + */ public function sql(): string { - return $this->select(); - /* - if(is_null($this->sql)) { - $sql = $this->buildSelect(); - $set = $this->set; - - if($this->prepare) { - $set = array_pad([], count($this->set), "?"); - - } - $this->sql = vsprintf($sql, $set); - } - //array_pad([], count($whereSet), "?"); - //$rawSql = vsprintf($rawSql, $this->set); - return $this->sql; - */ } /** @@ -82,7 +71,7 @@ public function sql(): string */ protected function getExplain(): string { - return ($this->db->__get('explain')) ? "EXPLAIN " : ""; + return ($this->db->explain) ? "EXPLAIN " : ""; } /** @@ -91,7 +80,7 @@ protected function getExplain(): string */ protected function getDistinct(): string { - return ($this->db->__get('distinct')) ? "DISTINCT " : ""; + return ($this->db->distinct) ? "DISTINCT " : ""; } /** @@ -100,7 +89,7 @@ protected function getDistinct(): string */ protected function getNoCache(): string { - return ($this->db->__get('noCache')) ? "SQL_NO_CACHE " : ""; + return ($this->db->noCache) ? "SQL_NO_CACHE " : ""; } /** @@ -109,11 +98,11 @@ protected function getNoCache(): string */ protected function getColumns(): string { - if(is_null($this->db->__get('columns'))) { + if(is_null($this->db->columns)) { return "*"; } $create = []; - $columns = $this->db->__get('columns'); + $columns = $this->db->columns; foreach($columns as $row) { $create[] = Helpers::addAlias($row['column'], $row['alias'], "AS"); } @@ -126,8 +115,8 @@ protected function getColumns(): string */ protected function getOrder(): string { - return (!is_null($this->db->__get('order'))) ? - " ORDER BY " . implode(",", Helpers::getOrderBy($this->db->__get('order'))) : ""; + return (!is_null($this->db->order)) ? + " ORDER BY " . implode(",", Helpers::getOrderBy($this->db->order)) : ""; } /** @@ -136,7 +125,7 @@ protected function getOrder(): string */ protected function getGroup(): string { - return (!is_null($this->db->__get('group'))) ? " GROUP BY " . implode(",", $this->db->__get('group')) : ""; + return (!is_null($this->db->group)) ? " GROUP BY " . implode(",", $this->db->group) : ""; } /** @@ -170,7 +159,7 @@ protected function getWhere(string $prefix, ?array $where, array &$set = []): st protected function getJoin(): string { $join = ""; - $data = $this->db->__get("join"); + $data = $this->db->join; foreach ($data as $row) { $table = Helpers::addAlias($row['table'], $row['alias']); $where = $this->getWhere("ON", $row['whereData']); @@ -185,12 +174,12 @@ protected function getJoin(): string */ protected function getLimit(): string { - $limit = $this->db->__get('limit'); - if (is_null($limit) && !is_null($this->db->__get('offset'))) { + $limit = $this->db->limit; + if (is_null($limit) && !is_null($this->db->offset)) { $limit = 1; } $limit = $this->getAttrValue($limit); - $offset = (!is_null($this->db->__get("offset"))) ? "," . $this->getAttrValue($this->db->__get("offset")) : ""; + $offset = (!is_null($this->db->offset)) ? "," . $this->getAttrValue($this->db->offset) : ""; return (!is_null($limit)) ? " LIMIT $limit $offset" : ""; } @@ -233,9 +222,13 @@ private function whereArrToStr(array $array, array &$set = []): string } - function getUnion(): string + /** + * Get Union sql + * @return string + */ + public function getUnion(): string { - $union = $this->db->__get('union'); + $union = $this->db->union; if(!is_null($union)) { $sql = ""; @@ -249,10 +242,14 @@ function getUnion(): string return ""; } + /** + * Get attribute as value item + * @param $value + * @return string|null + */ public function getAttrValue($value): ?string { - if($this->db->__get('prepare')) { - + 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); @@ -262,11 +259,12 @@ public function getAttrValue($value): ?string 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/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 717b542..04f87f5 100755 --- a/Utility/Attr.php +++ b/Utility/Attr.php @@ -4,26 +4,27 @@ 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; /** - * MAKE IMMUTABLE in future + * I might make it IMMUTABLE in future */ class Attr implements AttrInterface { - const RAW_TYPE = 0; - const VALUE_TYPE = 1; - const COLUMN_TYPE = 2; - const VALUE_TYPE_NUM = 3; - const VALUE_TYPE_STR = 4; + 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; private ConnectInterface|HandlerInterface $connection; private float|int|array|string|null $value = null; private float|int|array|string $raw; - private array $set = []; + //private array $set = []; //private bool $hasBeenEncoded = false; private int $type = 0; @@ -82,19 +83,23 @@ public function type(int $dataType): static $inst->value = (float)$inst->value; } - // Will not "prep" column type by default but instead it will "sanitize" + // 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; } + /** + * Check value type + * @param int $type + * @return int + */ public function isType(int $type): int { return ($this->type === $type); } - /** * IMMUTABLE: Enable/disable MySQL prep * @param bool $prep @@ -167,7 +172,6 @@ public function encode(bool $encode): static */ public function getValue(): string { - $inst = clone $this; if(is_null($inst->value)) { throw new BadMethodCallException("You need to set a value first with \"withValue\""); @@ -191,7 +195,10 @@ public function getValue(): string } if ($inst->enclose) { - $inst->value = ($inst->type === self::COLUMN_TYPE) ? $this->getValueToColumn() : "'$inst->value'"; + // 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; diff --git a/Utility/Helpers.php b/Utility/Helpers.php index dade171..90a5ae3 100644 --- a/Utility/Helpers.php +++ b/Utility/Helpers.php @@ -7,9 +7,8 @@ use MaplePHP\Query\Interfaces\AttrInterface; use MaplePHP\Query\Interfaces\DBInterface; - -class Helpers { - +class Helpers +{ protected const OPERATORS = [">", ">=", "<", "<>", "!=", "<=", "<=>"]; // Comparison operators protected const JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "CROSS"]; // Join types @@ -73,7 +72,7 @@ public static function getOrderBy(array $arr): array public static function buildJoinData(DBInterface $inst, string|array $where): array { - $data = array(); + $data = []; if (is_array($where)) { foreach ($where as $key => $val) { if (is_array($val)) { @@ -95,7 +94,7 @@ public static function buildJoinData(DBInterface $inst, string|array $where): ar return $data; } - function validateIdentifiers($column): bool + public function validateIdentifiers($column): bool { return (preg_match('/^[a-zA-Z0-9_]+$/', $column) !== false); } @@ -124,7 +123,7 @@ public static function prep(mixed $val, bool $enclose = true): AttrInterface */ public static function prepArr(array $arr, bool $enclose = true): array { - $new = array(); + $new = []; foreach ($arr as $pKey => $pVal) { $key = (string)static::prep($pKey, false); $new[$key] = (string)static::prep($pVal, $enclose); @@ -239,4 +238,4 @@ public static function toAlias(string|AttrInterface $table, null|string|AttrInte return $table; } -} \ No newline at end of file +} diff --git a/Utility/WhitelistMigration.php b/Utility/WhitelistMigration.php index 0eb6e17..4385016 100755 --- a/Utility/WhitelistMigration.php +++ b/Utility/WhitelistMigration.php @@ -1,4 +1,5 @@ =8.0", - "maplephp/dto": "^1.0" + "maplephp/dto": "^2.0" }, "require-dev": { "maplephp/unitary": "^1.0" 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 209c566..65f0145 100755 --- a/tests/unitary-db.php +++ b/tests/unitary-db.php @@ -13,59 +13,31 @@ // 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); - $unit->addTitle("Testing MaplePHP Query library!"); - - $unit->add("OK", function ($inst) use ($unit, $db) { - - $prepare = new Prepare(); - for($i = 1; $i < 6; $i++) { - - // - - /* - $test = $db->table("test") - ->columns("id", "name") - ->where("id", 1); - $test2 = $db->table("test") - ->columns("id", "name") - ->where("id", 3) - ->order("id", "DESC") - ->limit(20); - */ - $inst = $db->table(["test", "a"]) - ->join(["test_category", "cat"], ["cat.tid" => "a.id"]) - ->columns("a.id", "a.name", ['cat.name' => "test"]) - ->where("id", $i); - $prepare->query($inst); - /* - print_r($inst->fetch()); - echo "\n\n"; - //print_r($test->sql()); - die; - */ - } + $unit->case("OK", function () use ($unit) { - print_r($prepare->execute()); - die; + $this->add("Test", [ + "isString" => [], + "length" => [1, 200] + ]); + $this->add("Testwqd", [ + "length" => [1, 200] + ], "Test data type"); }); - $unit->execute(); -} +//} /*