diff --git a/.gitignore b/.gitignore index f56cefe..eb907e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,19 @@ -test -Experimental -vendor -composer.lock \ No newline at end of file +# Global +.phpunit* +.composer +composer.lock +package-lock.json +vendor/ +test/ +tests/ +*.tests.php + +# OS Generated +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db +*.swp + +# phpstorm +.idea/* diff --git a/README.md b/README.md index 755fd23..4054160 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,41 @@



-

Leaf Db module

+

Leaf Db v3



-# Leaf PHP - [![Latest Stable Version](https://poser.pugx.org/leafs/db/v/stable)](https://packagist.org/packages/leafs/db) [![Total Downloads](https://poser.pugx.org/leafs/db/downloads)](https://packagist.org/packages/leafs/db) [![License](https://poser.pugx.org/leafs/db/license)](https://packagist.org/packages/leafs/db) -Leaf PHP db feature packaged as a serve-yourself module. +Leaf DB has gone through yet another re-write. This time, Leaf DB focuses on maintaining a cleaner structure with more usable and grounded code. v3 supports more databases like postgres and sqlite, comes with some performance gains and is far more efficient than v1 and v2. It is also independent of the leaf core which makes it suitable for any project you run. + +## What's new? + +### DB Support + +Leaf DB now supports connections with other databases like postgresql, sqlite, oracle and more. + +### Deep syncing with leaf 3 + +Leaf DB is now detached from leaf, however, as a leaf 3 module, there's additional functionality you can get from using leaf db in a leaf 3 app. Deep syncing config, instances and functional mode all become available to you. + +### PDO rewrite + +Under the hood, Leaf DB has been rewritten to fully support PDO, both internally and user instantiated PDO instances. This makes leaf db more flexible and more compatible with most systems and applications. + +### Performance Improvements + +After a series of benchmarks with ApacheBench, apps using leaf db v3 were almost twice as fast as apps using the prior version. These small performance wins can go a long way to improve the overall perfomance of your app drastically. + +### Methods + +- `create` +- `drop` +- `insert` with multiple fields +- Connections with pgsql, oracle, sqlite and many more db types +- Functional mode ## Installation @@ -22,6 +46,29 @@ You can easily install Leaf using [Composer](https://getcomposer.org/). composer require leafs/db ``` -## View Leaf's docs [here](https://leafphp.netlify.app/#/) +## Basic usage + +After installing leaf db, you need to connect to your database to use any of the db functions. + +```php +$db = new Leaf\Db('127.0.0.1', 'dbName', 'user', 'password'); + +# or + +$db = new Leaf\Db(); +$db->connect('127.0.0.1', 'dbName', 'user', 'password'); +``` + +If you're using leaf db in a leaf 3 app, you will have access to the `db` global + +```php +db()->connect('127.0.0.1', 'dbName', 'user', 'password'); +``` + +From there, you can use any db method. + +```php +$users = db()->select('users')->all(); +``` -Built with ❤ by [**Mychi Darko**](https://mychi.netlify.app) +You can find leaf db's complete documentation [here](https://leafphp.dev/modules/db/). **The docs are still being updated.** diff --git a/composer.json b/composer.json index 63b6fb8..35982b8 100644 --- a/composer.json +++ b/composer.json @@ -22,12 +22,17 @@ "autoload": { "psr-4": { "Leaf\\": "src" - } + }, + "files": [ + "src/functions.php" + ] }, "minimum-stability": "dev", "prefer-stable": true, "require": { - "ext-mysqli": "*", - "leafs/form": "^1.0" + "ext-mysqli": "*" + }, + "require-dev": { + "pestphp/pest": "^1.21" } } diff --git a/src/Db.php b/src/Db.php old mode 100755 new mode 100644 index 562f803..0efcaf7 --- a/src/Db.php +++ b/src/Db.php @@ -1,811 +1,248 @@ "", - "type" => "", - "query" => "", - "bindings" => [], - "uniques" => [], - "validate" => [], - "values" => [], - "hidden" => [], - "add" => [] - ]; - - /** - * Query identifiers - */ - protected $identifiers = [ - "insert" => "INSERT INTO ", - "select" => "SELECT ", - "update" => "UPDATE ", - "delete" => "DELETE FROM " - ]; - - /** - * Query result - */ - protected $queryResult; - - /** - * Any errors caught - */ - protected $errorsArray = []; - - /** - * Leaf Form Module - */ - protected $form; - - /** - * List of methods called - */ - protected $callStack = []; - - public function __construct($host = null, $user = null, $password = null, $dbname = null) - { - $this->form = new Form; - - if ($host != null || $user != null || $password != null || $dbname != null) { - $this->connect($host, $user, $password, $dbname); - } - } - - /** - * Return the database connection - */ - public function connection() - { - return $this->connection; - } - - /** - * Connect to database - * - * @param string $host: Host Name - * @param string $user: Database username - * @param string $password: Database password - * @param string $dbname: Database name - */ - public function connect(string $host, string $user, string $password, string $dbname): void - { - try { - $connection = mysqli_connect($host, $user, $password, $dbname); - $this->connection = $connection; - } catch (\Exception $e) { - $this->connection = null; - $this->errorsArray["connection"] = $e->getMessage(); - } - - $this->callStack[] = "connect"; - } - - /** - * Connect to database using environment variables - */ - public function autoConnect(): void - { - $this->connect( - getenv("DB_HOST"), - getenv("DB_USERNAME"), - getenv("DB_PASSWORD"), - getenv("DB_DATABASE") - ); - - $this->callStack[] = "autoConnect"; - } - - /** - * Manually create a database query - * - * @param string $sql Full db query - */ - public function query(string $sql): self - { - $this->queryData["query"] = $sql; - - foreach ($this->identifiers as $key => $value) { - if (strpos(strtoupper($sql), $value) === 0) { - $this->queryData["type"] = $key; - break; - } - } - - if ($this->queryData["table"] === "") { - $data = explode(" ", $sql); - - if ($data[0] === "SELECT" || $data[0] === "UPDATE") { - $this->queryData["table"] = $data[1]; - } else { - $this->queryData["table"] = $data[2]; - } - } - - $this->callStack[] = "query"; - - return $this; - } - - /** - * Db Select - * - * Retrieve a row from table - * - * @param string $table: Db Table - * @param string $items: Specific table columns to fetch - */ - public function select(string $table, string $items = "*") - { - $this->query("SELECT $items FROM $table"); - $this->queryData["table"] = $table; - $this->callStack[] = "select"; - - return $this; - } - - /** - * Db Insert - * - * Add a new row in a db table - * - * @param string $table: Db Table - */ - public function insert(string $table): self - { - $this->query("INSERT INTO $table"); - $this->queryData["table"] = $table; - $this->callStack[] = "insert"; - - return $this; - } - - /** - * Db Update - * - * Update a row in a db table - * - * @param string $table: Db Table - */ - public function update(string $table): self - { - $this->query("UPDATE $table"); - $this->queryData["table"] = $table; - $this->callStack[] = "update"; - - return $this; - } - - /** - * Db Delete - * - * Delete a table's records - * - * @param string $table: Db Table - */ - public function delete(string $table): self - { - $this->query("DELETE FROM $table"); - $this->queryData["table"] = $table; - $this->callStack[] = "delete"; - - return $this; - } - - /** - * Pass in parameters into your query - * - * @param array $params Params to pass into query - */ - public function params(array $params): self - { - $query = $this->queryData["type"] == "update" ? " SET " : " "; - - $count = 0; - $dataToBind = []; - $keys = ""; - $values = ""; - - foreach ($params as $key => $value) { - if ($this->queryData["type"] == "insert") { - $keys .= $key; - $values .= "?"; - if ($count < count($params) - 1) { - $keys .= ", "; - $values .= ", "; - } - } else if ($this->queryData["type"] == "update") { - $query .= "$key = ?"; - if ($count < count($params) - 1) { - $query .= ", "; - } - } - $dataToBind[] = $value; - $count += 1; - } - - if ($this->queryData["type"] == "insert") { - $query .= "($keys) VALUES ($values)"; - } - - $this->bind($dataToBind); - $this->queryData["query"] .= $query; - $this->queryData["values"] = $params; - - $this->callStack[] = "params"; - - return $this; - } - - /** - * Controls inner workings of all where blocks - */ - protected function baseWhere($condition, $value = null, $comparator = "=", $operation = "AND") - { - $query = ""; - - if (!in_array("where", $this->callStack)) { - $query = " WHERE "; - } - - $count = 0; - $dataToBind = []; - $params = []; - $comparator ?? "="; - - if (is_array($condition)) { - foreach ($condition as $key => $value) { - $query .= "$key $comparator ?"; - if ($count < count($condition) - 1) { - $query .= " $operation "; - } - if ($this->queryData["type"] === "select" || $this->queryData["type"] === "delete") { - $params[$key] = $value; - } - $dataToBind[] = $value; - $count += 1; - } - } else { - if (!$value) { - $query .= $condition; - } else { - if ($this->queryData["type"] === "select" || $this->queryData["type"] === "delete") { - $params[$condition] = $value; - } - $query .= "$condition $comparator ?"; - $dataToBind[] = $value; - } - } - - $this->bind($dataToBind); - - if ($this->queryData["type"] === "select" || $this->queryData["type"] === "delete") { - $this->queryData["values"] = $params; - } - - $this->queryData["query"] .= $query; - $this->callStack[] = "where"; - - return $this; - } - - /** - * Add a where clause to db query - * - * @param string|array $condition - * @param string|null $value - */ - public function where($condition, $value = null): self - { - return $this->baseWhere($condition, $value); - } - - /** - * Controls inner workings of orWhere - */ - protected function baseOrWhere($condition, $value = null, $operation = "=") - { - if (in_array("where", $this->callStack)) { - $this->queryData["query"] .= " OR "; - } - - $this->callStack[] = "orWhere"; - return $this->baseWhere($condition, $value, $operation, "OR"); - } - - /** - * Add a where clause with OR comparator to db query - * - * @param string|array $condition - * @param string|null $value - */ - public function orWhere($condition, $value = null): self - { - return $this->baseOrWhere($condition, $value); - } - - /** - * Add a where clause with LIKE comparator to db query - * - * @param string|array $condition - * @param string|null $value - */ - public function whereLike($condition, $value = null): self - { - $this->callStack[] = "whereLike"; - return $this->baseWhere($condition, $value, "LIKE"); - } - - /** - * Add a where clause with LIKE comparator to db query - * - * @param string|array $condition - * @param string|null $value - */ - public function orWhereLike($condition, $value = null): self - { - $this->callStack[] = "orWhereLike"; - return $this->orWhere($condition, $value, "LIKE"); - } - - /** - * Alias for `whereLike` - * - * @param string|array $condition - * @param string|null $value - */ - public function like($condition, $value = null): self - { - $this->callStack[] = "like"; - return $this->whereLike($condition, $value); - } - - /** - * Alias for `orWhereLike` - * - * @param string|array $condition - * @param string|null $value - */ - public function orLike($condition, $value = null): self - { - $this->callStack[] = "orLike"; - return $this->orWhereLike($condition, $value); - } - - /** - * Set a max number of resources - * - * @param mixed $limit The number of rows to fetch - */ - public function limit($limit): self - { - $this->queryData["query"] .= " LIMIT $limit"; - - $this->callStack[] = "limit"; - return $this; - } - - /** - * Order results according to key - * - * @param string $key The key to order results by - * @param string $direction The direction to order [DESC, ASC] - */ - public function orderBy($key, $direction = "desc"): self - { - $direction = strtoupper($direction); - $this->queryData["query"] .= " ORDER BY $key $direction"; - - $this->callStack[] = "orderBy"; - return $this; - } - - /** - * Validate data before running a query - * - * @param array|string $item The item(s) to validate - * @param string|null $rule The validation rule to apply - */ - public function validate($item, $rule = "required"): self - { - $values = $this->queryData["values"]; - - if (is_array($item)) { - foreach ($item as $key => $value) { - $this->queryData["validate"][] = [$key, $values[$key], strtolower($value) ?? "required"]; - } - } else { - $this->queryData["validate"][] = [$item, $values[$item], strtolower($rule)]; - } - return $this; - } - - /** - * Make sure a value doesn't already exist in a table to avoid duplicates. - * - * @param mixed $uniques Items to check for - */ - public function unique(...$uniques) - { - $data = []; - foreach ($uniques as $unique) { - if (is_array($unique)) { - $data = $unique; - } else { - $data[] = $unique; - } - } - $this->queryData["uniques"] = $data; - - return $this; - } - - /** - * Hide particular fields from the final value returned - * - * @param mixed $values The value(s) to hide - */ - public function hidden(...$values): self - { - $data = []; - foreach ($values as $value) { - if (is_array($value)) { - $data = $value; - } else { - $data[] = $value; - } - } - $this->queryData["hidden"] = $data; - - return $this; - } - - /** - * Add particular fields to the final value returned - * - * @param string|array $name What to add - * @param string $value The value to add - */ - public function add($name, $value = null): self - { - $data = []; - if (is_array($name)) { - $data = $name; - } else { - $data[$name] = $value; - } - $this->queryData["add"] = $data; - - return $this; - } - - /** - * Bind parameters to a query - * - * @param array|string $data The data to bind to string - */ - public function bind(...$bindings): self - { - $data = []; - foreach ($bindings as $binding) { - if (is_array($binding)) { - $data = $binding; - } else { - $data[] = $binding; - } - } - - $this->queryData["bindings"] = array_merge($this->queryData["bindings"], $data); - - return $this; - } - - /** - * Execute a query - * - * @param array $paramTypes The types for parameters(defaults to strings) - * - * @return null|void - */ - public function execute($paramTypes = null) - { - if ($this->connection === null) { - trigger_error("Couldn't establish database connection. Call the connect() method, or check your database"); - } - - if (count($this->errorsArray) > 0) return null; - - $query = $this->queryData["query"]; - $bindings = $this->queryData["bindings"]; - $paramValues = $this->queryData["values"]; - $uniques = $this->queryData["uniques"]; - $validate = $this->queryData["validate"]; - - if (count($validate) > 0) { - foreach ($validate as $item) { - if (!$this->form->validateField($item[0], $item[1], $item[2])) { - foreach ($this->form->errors() as $name => $error) { - $this->errorsArray[$name] = $error; - } - } - } - - if (count($this->errorsArray) > 0) return null; - } - - if (count($uniques) > 0 && ($this->queryData["type"] != "select" || $this->queryData["type"] != "delete")) { - foreach ($uniques as $unique) { - if (!isset($paramValues[$unique])) { - trigger_error("$unique not found, Add $unique to your \$db->add items or check your spelling."); - } - - if (mysqli_fetch_object($this->connection->query("SELECT * FROM {$this->queryData["table"]} WHERE $unique = '$paramValues[$unique]'"))) { - $this->errorsArray[$unique] = "$unique already exists"; - } - } - - if (count($this->errorsArray) > 0) return null; - } - - if (!$bindings || count($bindings) === 0) { - try { - $this->queryResult = $this->connection->query($query); - } catch (\Throwable $th) { - $this->errorsArray["query"] = $th->getMessage(); - } - } else { - $stmt = $this->stmt = $this->connection->prepare($query); - $stmt->bind_param($paramTypes ?? str_repeat('s', count($bindings)), ...$bindings); - try { - $stmt->execute(); - } catch (\Throwable $th) { - $this->errorsArray["query"] = $th->getMessage(); - } - $this->queryResult = $stmt->get_result(); - } - - if ($this->queryData["type"] !== "select") { - $this->clearState(); - } - $this->callStack = []; - - return true; - } - - /** - * Get number of rows from SELECT - * - * @return int|null $connection->num_rows - */ - public function count(): ?int + /** + * Create a database if it doesn't exist + * + * @param string $db The name of the database to create + */ + public function create(string $db): self { - if (!$this->execute()) return null; - $this->clearState(); - - return mysqli_num_rows($this->queryResult); - } - - /** - * Fetch query results as an associative array - */ - public function fetchAssoc() - { - if (!$this->execute()) return null; - $result = mysqli_fetch_assoc($this->queryResult); - - $add = $this->queryData["add"]; - if (count($add) > 0) { - foreach ($add as $item => $value) { - $result[$item] = $value; - } - } - - $hidden = $this->queryData["hidden"]; - - if (count($hidden) > 0) { - foreach ($hidden as $item) { - if (isset($result[$item]) || $result[$item] === null) unset($result[$item]); - } - } - - $this->clearState(); - return $result; - } - - /** - * Fetch query results as object - */ - public function fetchObj() - { - if (!$this->execute()) return null; - $result = mysqli_fetch_object($this->queryResult); - - $add = $this->queryData["add"]; - if (count($add) > 0) { - foreach ($add as $item => $value) { - $result->{$item} = $value; - } - } - - $hidden = $this->queryData["hidden"]; - if (count($hidden) > 0) { - foreach ($hidden as $item) { - if (isset($result->{$item})) unset($result->{$item}); - } - } - - $this->clearState(); - return $result; - } - - /** - * Fetch all - */ - public function fetchAll(): ?array + $this->query("CREATE DATABASE $db"); + return $this; + } + + /** + * Drop a database if it exists + * + * @param string $db The name of the database to drop + */ + public function drop(string $db): self { - if (!$this->execute()) return null; - $result = mysqli_fetch_all($this->queryResult, \MYSQLI_ASSOC); - - $add = $this->queryData["add"]; - $hidden = $this->queryData["hidden"]; - $final = []; - - if (count($add) > 0 || count($hidden) > 0) { - foreach ($result as $res) { - if (count($add) > 0) { - foreach ($add as $item => $value) { - $res[$item] = $value; - } - } - - if (count($hidden) > 0) { - foreach ($hidden as $item) { - if (isset($res[$item])) unset($res[$item]); - } - } - $final[] = $res; - } - } else { - $final = $result; - } - - $this->clearState(); - return $final; - } - - /** - * Alias of fetchAll - */ - public function all(): ?array + $this->query("DROP DATABASE $db"); + return $this; + } + + /** + * Add a find by id clause to query + * + * @param string|int $id The id of the row to find + */ + public function find($id) { - return $this->fetchAll(); - } - - /** - * Get first matching result - */ - public function first() - { - $result = $this->fetchAll(); - return $result[0] ?? $result; - } - - /** - * Get last matching result - */ - public function last() - { - $result = $this->fetchAll(); - return $result[count($result) - 1] ?? $result; - } - - /** - * Return raw query result - */ - public function fetch(): ?array - { - if (!$this->execute()) return null; - return $this->queryResult; - } - - /** - * Set the current db table - */ - public function table($table): Db + $this->where('id', $id); + return $this->first(); + } + + /** + * Find the first matching item for current query + */ + public function first() { - $this->queryData["table"] = $table; - - return $this; - } - - /** - * Search a db table for a value - */ - public function search($row, $value, $hidden = null): ?array + $this->query .= ' ORDER BY id ASC LIMIT 1'; + return $this->fetchAssoc(); + } + + /** + * Find the last matching item for current query + */ + public function last() { - return $this->select($this->queryData["table"])->like($row, static::includes($value))->hidden($hidden)->all(); - } - - /** - * Closes MySQL connection - */ - public function close(): void - { - $this->connection->close(); - } - - /** - * Return caught errors if any - */ - public function errors(): array - { - return $this->errorsArray; - } - - protected function clearState() - { - $this->queryData = [ - "table" => "", - "type" => "", - "query" => "", - "bindings" => [], - "uniques" => [], - "validate" => [], - "values" => [], - "hidden" => [], - "add" => [] - ]; - $this->callStack = []; - } - - /** - * Construct search that begins with a phrase in db - */ - public static function beginsWith($phrase): string + $this->query .= ' ORDER BY id DESC LIMIT 1'; + return $this->fetchAssoc(); + } + + /** + * Order query items by a specific + * + * @param string $column The column to order results by + * @param string $direction The direction to order [DESC, ASC] + */ + public function orderBy(string $column, string $direction = 'desc') { - return "$phrase%"; - } - - /** - * Construct search that ends with a phrase in db - */ - public static function endsWith($phrase): string + $this->query = Builder::orderBy($this->query, $column, $direction); + return $this; + } + + /** + * Limit query items by a specific number + * + * @param string|number $limit The number to limit by + */ + public function limit($limit) { - return "%$phrase"; - } - - /** - * Construct search that includes a phrase in db - */ - public static function includes($phrase): string + $this->query = Builder::limit($this->query, $limit); + return $this; + } + + /** + * Retrieve a row from table + * + * @param string $table Db Table + * @param string $items Specific table columns to fetch + */ + public function select(string $table, string $items = "*") + { + $this->query("SELECT $items FROM $table"); + $this->table = $table; + return $this; + } + + /** + * Add a new row in a db table + * + * @param string $table Db Table + */ + public function insert(string $table): self + { + $this->query("INSERT INTO $table"); + $this->table = $table; + return $this; + } + + /** + * Update a row in a db table + * + * @param string $table Db Table + */ + public function update(string $table): self { - return "%$phrase%"; - } + $this->query("UPDATE $table"); + $this->table = $table; + return $this; + } + + /** + * Delete a table's records + * + * @param string $table: Db Table + */ + public function delete(string $table): self + { + $this->query("DELETE FROM $table"); + $this->table = $table; + return $this; + } - /** - * Construct search that begins and ends with a phrase in db + /** + * Pass in parameters into your query + * + * @param array|string $params Key or params to pass into query + * @param string|null $value Value for key */ - public static function word($beginsWith, $endsWith): string + public function params($params): self + { + $this->query = Builder::params($this->query, $params); + $this->bind(...(Builder::$bindings)); + $this->params = $params; + return $this; + } + + /** + * Add a where clause to db query + * + * @param string|array $condition The condition to evaluate + * @param mixed $comparator Condition value or comparator + * @param mixed $value The value of condition if comparator is passed + */ + public function where($condition, $comparator = null, $value = null): self + { + $this->query = Builder::where( + $this->query, + $condition, + $value === null ? $comparator : $value, + $value === null ? "=" : $comparator + ); + $this->bind(...(Builder::$bindings)); + + return $this; + } + + /** + * Add a where clause with OR comparator to db query + * + * @param string|array $condition The condition to evaluate + * @param mixed $comparator Condition value or comparator + * @param mixed $value The value of condition if comparator is passed + */ + public function orWhere($condition, $comparator = null, $value = null): self + { + $this->query = Builder::where( + $this->query, + $condition, + $value === null ? $comparator : $value, + $value === null ? "=" : $comparator, + "OR" + ); + $this->bind(...(Builder::$bindings)); + + return $this; + } + + /** + * Hide particular fields from the final value returned + * + * @param mixed $values The value(s) to hide + */ + public function hidden(...$values): self + { + $this->hidden = Utils::flatten($values); + return $this; + } + + /** + * Make sure a value doesn't already exist in a table to avoid duplicates. + * + * @param mixed $uniques Items to check for + */ + public function unique(...$uniques) + { + $this->uniques = Utils::flatten($uniques); + return $this; + } + + /** + * Add particular fields to the final value returned + * + * @param string|array $name What to add + * @param string $value The value to add + */ + public function add($name, $value = null): self + { + if (is_array($name)) { + $this->added = $name; + } else { + $this->added[$name] = $value; + } + + return $this; + } + + /** + * Search a db table for a value + * + * @param string $row The item to search for in table + * @param string $value The keyword to search for + * @param array|null $hidden The items to hide from returned result + */ + public function search(string $row, string $value, ?array $hidden = []): ?array { - return "$beginsWith%$endsWith"; - } + return $this->select($this->table)->where($row, 'LIKE', Utils::includes($value))->hidden($hidden)->all(); + } } diff --git a/src/Db/Builder.php b/src/Db/Builder.php new file mode 100644 index 0000000..dfef369 --- /dev/null +++ b/src/Db/Builder.php @@ -0,0 +1,160 @@ + $v) { + $rebuild[$k] = '?'; + } + + $query .= str_replace('%3F', '?', http_build_query($rebuild, '', ' AND ')); + static::$bindings = array_values($params); + } + + return $query; + } +} diff --git a/src/Db/Core.php b/src/Db/Core.php new file mode 100644 index 0000000..d0ae1d3 --- /dev/null +++ b/src/Db/Core.php @@ -0,0 +1,506 @@ + 'mysql', + 'charset' => null, + 'port' => null, + 'unixSocket' => null, + 'host' => '127.0.0.1', + 'username' => 'root', + 'password' => '', + 'dbname' => '', + ]; + + /** + * Db table to peform operations on + */ + protected $table = null; + + /** + * leaf db connection instance + */ + protected $connection = null; + + /** + * Errors caught in leaf db + */ + protected $errors = []; + + /** + * Actual query to run + */ + protected $query; + + /** + * Full list of params passed into leaf db + */ + protected $params = []; + + /** + * Params bound to query + */ + protected $bindings = []; + + /** + * Items to hide from query results + */ + protected $hidden = []; + + /** + * Items to add to query results + */ + protected $added = []; + + /** + * Items which should be unique in db + */ + protected $uniques = []; + + /** + * Query result + * + * @var \PDOStatement + */ + protected $queryResult; + + /** + * Initialize leaf db with a database connection + * + * @param string|array $host Host Name or full config + * @param string $dbname Database name + * @param string $user Database username + * @param string $password Database password + * @param string $dbtype Type of database: mysql, postgres, sqlite, ... + */ + public function __construct( + $host = '', + string $dbname = '', + string $user = '', + string $password = '', + string $dbtype = 'mysql' + ) { + if (class_exists('Leaf\App')) app()->config('db', $this->config); + + if ($host !== '') { + $this->connect($host, $dbname, $user, $password, $dbtype); + } + } + + /** + * Connect to database + * + * @param string|array $host Host Name or full config + * @param string $dbname Database name + * @param string $user Database username + * @param string $password Database password + * @param string $dbtype Type of database: mysql, postgres, sqlite, ... + * @param array $pdoOptions Options for PDO connection + */ + public function connect( + $host = '', + string $dbname = '', + string $user = '', + string $password = '', + string $dbtype = '', + array $pdoOptions = [] + ): \PDO { + try { + $dbtype = $dbtype !== '' ? $dbtype : $this->config('dbtype'); + $dsn = $this->dsn($host, $dbname, $dbtype); + + $connection = new \PDO( + $dsn, + $dbtype === 'sqlite' ? null : ($user !== '' ? $user : $this->config('username')), + $dbtype === 'sqlite' ? null : ($password !== '' ? $password : $this->config('password')), + array_merge( + $this->config('pdoOptions') ?? [], + $pdoOptions + ) + ); + + $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->connection = $connection; + + return $connection; + } catch (\Throwable $th) { + throw $th; + } + } + + /** + * Connect to database using environment variables + * + * @param array $pdoOptions Options for PDO connection + */ + public function autoConnect(array $pdoOptions = []): \PDO + { + return $this->connect( + getenv('DB_HOST'), + getenv('DB_DATABASE'), + getenv('DB_USERNAME'), + getenv('DB_PASSWORD'), + getenv('DB_CONNECTION') ? getenv('DB_CONNECTION') : 'mysql', + $pdoOptions, + ); + } + + protected function dsn( + $host = '', + string $dbname = '', + string $dbtype = '' + ): string { + if ($dbtype === 'sqlite') { + $dsn = "sqlite:$dbname"; + } else { + $dbhost = $host !== '' ? $host : $this->config('host'); + $dbtype = $dbtype !== '' ? $dbtype : 'mysql'; + + $dsn = "$dbtype:host=$dbhost"; + + if ($dbname !== '') $dsn .= ";dbname=$dbname"; + if ($this->config('port')) $dsn .= ';port=' . $this->config('port'); + if ($this->config('charset')) $dsn .= ';charset=' . $this->config('charset'); + if ($this->config('unixSocket')) $dsn .= ';unix_socket=' . $this->config('unixSocket'); + } + + return $dsn; + } + + /** + * Return the database connection + * + * @param \PDO $connection Manual instance of PDO connection + */ + public function connection(\PDO $connection = null) + { + if (!$connection) return $this->connection; + $this->connection = $connection; + } + + /** + * Closes Db connection + */ + public function close(): void + { + $this->connection = null; + } + + /** + * Set the current db table for operations + * + * @param string $table Table to perform database operations on + */ + public function table(string $table): self + { + $this->table = $table; + return $this; + } + + /** + * Configure leaf db - syncs with leaf config + */ + public function config($name, $value = null) + { + if (class_exists('Leaf\App') && function_exists('app')) { + if (is_array($name)) { + foreach ($name as $key => $v) { + app()->config("db.$key", $v); + } + } else { + return app()->config("db.$name", $value); + } + } else { + if (is_array($name)) { + $this->config = array_merge($name, $this->config); + } else { + if (!$value) { + return $this->config[$name]; + } else { + $this->config[$name] = $value; + } + } + } + } + + /** + * Manually create a database query + * + * @param string $sql Full db query + */ + public function query(string $sql): self + { + $this->query = $sql; + return $this; + } + + /** + * Bind parameters to a query + * + * @param array|string $data The data to bind to string + */ + public function bind(...$bindings): self + { + $this->bindings = $bindings; + return $this; + } + + /** + * Execute a generated query + */ + public function execute() + { + if ($this->connection === null) trigger_error('Initialise your database first with connect()'); + + $state = $this->copyState(); + $this->clearState(); + + if (count($state['uniques'])) { + $IS_UPDATE = is_int(strpos($state['query'], 'UPDATE ')); + $IS_INSERT = is_int(strpos($state['query'], 'INSERT INTO ')); + + if ($IS_UPDATE || $IS_INSERT) { + foreach ($state['uniques'] as $unique) { + if (!isset($state['params'][$unique])) { + trigger_error("$unique not found, Add $unique to your insert or update items or check your spelling."); + } + } + + if ($this->connection->query("SELECT * FROM {$state['table']} WHERE $unique='{$state['params'][$unique]}'")->fetch(\PDO::FETCH_ASSOC)) { + $this->errors[$unique] = "$unique already exists"; + } + + if (count($this->errors)) { + Builder::$bindings = []; + return null; + } + } + } + + if (count($state['bindings']) === 0) { + $this->queryResult = $this->connection->query($state['query']); + } else { + $stmt = $this->connection->prepare($state['query']); + $stmt->execute($state['bindings']); + + $this->queryResult = $stmt; + } + + Builder::$bindings = []; + + return $this->queryResult; + } + + /** + * Get raw result of last query + * + * @return \PDOStatement + */ + public function result() + { + $this->execute(); + return $this->queryResult; + } + + /** + * Fetch column from results + */ + public function column() + { + $this->execute(); + return $this->queryResult->fetch(\PDO::FETCH_COLUMN); + } + + /** + * Get the current count of objects in query + */ + public function count(): int + { + $this->execute(); + return $this->queryResult->rowCount(); + } + + /** + * Alias for fetchAssoc + */ + public function assoc() + { + return $this->fetchAssoc(); + } + + /** + * Fetch the items returned by query + */ + public function fetchAssoc() + { + $added = $this->added; + $hidden = $this->hidden; + + $this->execute(); + $result = $this->queryResult->fetch(\PDO::FETCH_ASSOC); + + if (count($added)) { + $result = array_merge($result, $added); + } + + if (count($hidden)) { + foreach ($hidden as $item) { + unset($result[$item]); + } + } + + return $result; + } + + /** + * Alias for fetchObj + */ + public function obj() + { + return $this->fetchObj(); + } + + /** + * Fetch the items returned by query + */ + public function fetchObj() + { + $add = $this->added; + $hidden = $this->hidden; + + $this->execute(); + $result = $this->queryResult->fetch(\PDO::FETCH_ASSOC); + + if (count($add)) { + $result = array_merge($result, $add); + } + + if (count($hidden)) { + foreach ($hidden as $item) { + unset($result[$item]); + } + } + + return (object) $result; + } + + /** + * Fetch the items returned by query + */ + public function fetchAll($type = 'assoc') + { + $added = $this->added; + $hidden = $this->hidden; + + $this->execute(); + + $results = array_map(function ($result) use($hidden, $added) { + if (count($hidden)) { + foreach ($hidden as $item) { + unset($result[$item]); + } + } + + if (count($added)) { + $result = array_merge($result, $added); + } + + return $result; + }, $this->queryResult->fetchAll(\PDO::FETCH_ASSOC)); + + if ($type == 'obj' || $type == 'object') { + $results = (object) $results; + } + + return $results; + } + + /** + * Alias for fetchAll + */ + public function all($type = 'assoc') + { + return $this->fetchAll($type); + } + + /** + * Alias for fetchAll + */ + public function get($type = 'assoc') + { + return $this->fetchAll($type); + } + + /** + * Copy internal state + */ + protected function copyState() + { + return [ + 'table' => $this->table, + 'query' => $this->query, + 'bindings' => $this->bindings, + 'uniques' => $this->uniques, + 'hidden' => $this->hidden, + 'added' => $this->added, + 'params' => $this->params, + ]; + } + + /** + * Prepare leaf db to handle next query + */ + protected function clearState() + { + $this->table = ""; + $this->query = ""; + $this->bindings = []; + $this->uniques = []; + $this->hidden = []; + $this->added = []; + $this->params = []; + } + + /** + * Get the current snapshot of leaf db internals + */ + public function debug() + { + return [ + 'query' => $this->query, + 'queryResult' => $this->queryResult, + 'config' => $this->config, + 'connection' => $this->connection, + 'bindings' => $this->bindings, + 'hidden' => $this->hidden, + 'added' => $this->added, + 'uniques' => $this->uniques, + 'errors' => $this->errors, + ]; + } + + /** + * Return caught errors if any + */ + public function errors(): array + { + return $this->errors; + } +} diff --git a/src/Db/Utils.php b/src/Db/Utils.php new file mode 100644 index 0000000..fb00dc9 --- /dev/null +++ b/src/Db/Utils.php @@ -0,0 +1,67 @@ +