From 0404bca02b3599be57a42f6b6025a591f273ca6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammet=20=C5=9EAFAK?= Date: Sun, 3 Dec 2023 20:48:08 +0300 Subject: [PATCH 1/2] v3 --- README.md | 167 +- composer.json | 6 +- src/Connection/Connection.php | 146 ++ .../Exceptions/ConnectionException.php | 8 + .../Interfaces/ConnectionInterface.php | 60 + src/DBAL/CRUD.php | 165 ++ src/DBAL/Database.php | 292 +++ .../Exceptions/SQLQueryExecuteException.php | 9 + src/DBAL/Interfaces/CRUDInterface.php | 82 + src/DBAL/Interfaces/DatabaseInterface.php | 99 + src/DBAL/Interfaces/ResultInterface.php | 82 + src/DBAL/Result.php | 144 ++ src/Database.php | 844 --------- src/Exceptions/ConnectionException.php | 18 - src/Exceptions/DeletableException.php | 18 - src/Exceptions/ModelCallbacksException.php | 18 - src/Exceptions/ModelException.php | 18 - src/Exceptions/ModelRelationsException.php | 18 - src/Exceptions/QueryBuilderException.php | 18 - src/Exceptions/QueryGeneratorException.php | 18 - src/Exceptions/ReadableException.php | 18 - src/Exceptions/SQLQueryExecuteException.php | 18 - src/Exceptions/UpdatableException.php | 18 - src/Exceptions/ValidationException.php | 18 - src/Exceptions/ValueException.php | 18 - src/Exceptions/WritableException.php | 18 - src/Facade/DB.php | 271 +-- src/Helpers/Helper.php | 123 -- src/Helpers/Parameters.php | 72 - src/Helpers/Validation.php | 342 ---- src/Init.php | 37 - src/Model.php | 627 ------- src/{ => ORM}/Entity.php | 74 +- src/ORM/Exceptions/DeletableException.php | 7 + src/ORM/Exceptions/EntityNotMethod.php | 14 + src/ORM/Exceptions/ModelException.php | 10 + src/ORM/Exceptions/ReadableException.php | 7 + src/ORM/Exceptions/UpdatableException.php | 7 + src/ORM/Exceptions/WritableException.php | 7 + src/ORM/Interfaces/EntityInterface.php | 18 + src/ORM/Interfaces/ModelInterface.php | 107 ++ src/ORM/Model.php | 248 +++ src/QueryBuilder.php | 1618 ----------------- .../Exceptions/QueryBuilderException.php | 8 + .../Exceptions/QueryGeneratorException.php | 9 + .../Interfaces/ParameterInterface.php | 48 + .../Interfaces/QueryBuilderInterface.php | 758 ++++++++ src/QueryBuilder/Parameters.php | 104 ++ src/QueryBuilder/QueryBuilder.php | 1455 +++++++++++++++ src/QueryBuilder/RawQuery.php | 53 + src/Raw.php | 41 - src/Result.php | 180 -- src/Utils/Datatables.php | 323 ++-- src/Utils/Helper.php | 37 + src/Utils/Pagination.php | 230 --- tests/QueryBuilderUnitTest.php | 155 +- 56 files changed, 4516 insertions(+), 4812 deletions(-) create mode 100644 src/Connection/Connection.php create mode 100644 src/Connection/Exceptions/ConnectionException.php create mode 100644 src/Connection/Interfaces/ConnectionInterface.php create mode 100644 src/DBAL/CRUD.php create mode 100644 src/DBAL/Database.php create mode 100644 src/DBAL/Exceptions/SQLQueryExecuteException.php create mode 100644 src/DBAL/Interfaces/CRUDInterface.php create mode 100644 src/DBAL/Interfaces/DatabaseInterface.php create mode 100644 src/DBAL/Interfaces/ResultInterface.php create mode 100644 src/DBAL/Result.php delete mode 100644 src/Database.php delete mode 100644 src/Exceptions/ConnectionException.php delete mode 100644 src/Exceptions/DeletableException.php delete mode 100644 src/Exceptions/ModelCallbacksException.php delete mode 100644 src/Exceptions/ModelException.php delete mode 100644 src/Exceptions/ModelRelationsException.php delete mode 100644 src/Exceptions/QueryBuilderException.php delete mode 100644 src/Exceptions/QueryGeneratorException.php delete mode 100644 src/Exceptions/ReadableException.php delete mode 100644 src/Exceptions/SQLQueryExecuteException.php delete mode 100644 src/Exceptions/UpdatableException.php delete mode 100644 src/Exceptions/ValidationException.php delete mode 100644 src/Exceptions/ValueException.php delete mode 100644 src/Exceptions/WritableException.php delete mode 100644 src/Helpers/Helper.php delete mode 100644 src/Helpers/Parameters.php delete mode 100644 src/Helpers/Validation.php delete mode 100644 src/Init.php delete mode 100644 src/Model.php rename src/{ => ORM}/Entity.php (59%) create mode 100644 src/ORM/Exceptions/DeletableException.php create mode 100644 src/ORM/Exceptions/EntityNotMethod.php create mode 100644 src/ORM/Exceptions/ModelException.php create mode 100644 src/ORM/Exceptions/ReadableException.php create mode 100644 src/ORM/Exceptions/UpdatableException.php create mode 100644 src/ORM/Exceptions/WritableException.php create mode 100644 src/ORM/Interfaces/EntityInterface.php create mode 100644 src/ORM/Interfaces/ModelInterface.php create mode 100644 src/ORM/Model.php delete mode 100644 src/QueryBuilder.php create mode 100644 src/QueryBuilder/Exceptions/QueryBuilderException.php create mode 100644 src/QueryBuilder/Exceptions/QueryGeneratorException.php create mode 100644 src/QueryBuilder/Interfaces/ParameterInterface.php create mode 100644 src/QueryBuilder/Interfaces/QueryBuilderInterface.php create mode 100644 src/QueryBuilder/Parameters.php create mode 100644 src/QueryBuilder/QueryBuilder.php create mode 100644 src/QueryBuilder/RawQuery.php delete mode 100644 src/Raw.php delete mode 100644 src/Result.php create mode 100644 src/Utils/Helper.php delete mode 100644 src/Utils/Pagination.php diff --git a/README.md b/README.md index 159d687..98f6c8c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Manage your database with or without abstraction. This library is built on the P ## Requirements -- PHP 7.4 and later. +- PHP 8.0 and later. - PHP PDO extension. ## Supported Databases @@ -20,15 +20,9 @@ Databases supported by PDO and suitable drivers are available at [https://www.ph composer require initphp/database ``` -or include the `src/init.php` file from this repo in your system. - -```php -require_once "src/Init.php"; -``` - ## Usage -### QueryBuilder and CRUD +### QueryBuilder & DBAL and CRUD ```php require_once "vendor/autoload.php"; @@ -53,8 +47,7 @@ $data = [ 'content' => 'Post Content', ]; -$isInsert = DB::table('post') - ->create($data); +$isInsert = DB::create('post', $data); /** * This executes the following query. @@ -67,8 +60,7 @@ $isInsert = DB::table('post') if($isInsert){ // Success } else { - $errors = DB::getError(); - foreach ($errors as $errMsg) { + foreach (DB::getErrors() as $errMsg) { echo $errMsg; } } @@ -91,8 +83,7 @@ $data = [ ], ]; -$isInsert = DB::table('post') - ->createBatch($data); +$isInsert = DB::createBatch('post', $data); /** * This executes the following query. @@ -107,8 +98,7 @@ $isInsert = DB::table('post') if($isInsert){ // Success } else { - $errors = DB::getError(); - foreach ($errors as $errMsg) { + foreach (DB::getErrors() as $errMsg) { echo $errMsg; } } @@ -119,14 +109,7 @@ if($isInsert){ ```php use \InitPHP\Database\Facade\DB; -DB::select('user.name as author_name', 'post.id', 'post.title') - ->from('post') - ->selfJoin('user', 'user.id=post.author') - ->where('post.status', 1) - ->orderBy('post.id', 'ASC') - ->orderBy('post.created_at', 'DESC') - ->offset(20)->limit(10); - + /** * This executes the following query. * @@ -136,11 +119,19 @@ DB::select('user.name as author_name', 'post.id', 'post.title') * ORDER BY post ASC, post.created_at DESC * LIMIT 20, 10 */ -$res = DB::read(); +$res = DB::select('user.name as author_name', 'post.id', 'post.title') + ->from('post') + ->selfJoin('user', 'user.id=post.author') + ->where('post.status', 1) + ->orderBy('post.id', 'ASC') + ->orderBy('post.created_at', 'DESC') + ->offset(20)->limit(10) + ->read('post'); + if($res->numRows() > 0){ $results = $res->asAssoc() - ->results(); + ->rows(); foreach ($results as $row) { echo $row['title'] . ' by ' . $row['author_name'] . '
'; } @@ -156,9 +147,8 @@ $data = [ 'content' => 'New Content', ]; -$isUpdate = DB::from('post') - ->where('id', 13) - ->update($data); +$isUpdate = DB::where('id', 13) + ->update('post', $data); /** * This executes the following query. @@ -170,8 +160,7 @@ $isUpdate = DB::from('post') if ($isUpdate) { // Success } else { - $errors = DB::getError(); - foreach ($errors as $errMsg) { + foreach (DB::getErrors() as $errMsg) { echo $errMsg; } } @@ -193,9 +182,8 @@ $data = [ ] ]; -$isUpdate = DB::from('post') - ->where('status', 1) - ->updateBatch($data, 'id'); +$isUpdate = DB::where('status', '!=', 0) + ->updateBatch('post', $data, 'id'); /** * This executes the following query. @@ -208,13 +196,12 @@ $isUpdate = DB::from('post') * content = CASE * WHEN id = 5 THEN 'New Content #5' * ELSE content END -* WHERE status = 1 AND id IN (5, 10) +* WHERE status != 0 AND id IN (5, 10) */ if ($isUpdate) { // Success } else { - $errors = DB::getError(); - foreach ($errors as $errMsg) { + foreach (DB::getErrors() as $errMsg) { echo $errMsg; } } @@ -225,9 +212,8 @@ if ($isUpdate) { ```php use \InitPHP\Database\Facade\DB; -$isDelete = DB::from('post') - ->where('id', 13) - ->delete(); +$isDelete = DB::where('id', 13) + ->delete('post'); /** * This executes the following query. @@ -237,8 +223,7 @@ $isDelete = DB::from('post') if ($isUpdate) { // Success } else { - $errors = DB::getError(); - foreach ($errors as $errMsg) { + foreach (DB::getErrors() as $errMsg) { echo $errMsg; } } @@ -263,13 +248,15 @@ $res = DB::select(DB::raw("CONCAT(name, ' ', surname) AS fullname")) ->where(DB::raw("title = '' AND (status = 1 OR status = 0)")) ->limit(5) ->get('users'); + /** * SELECT CONCAT(name, ' ', surname) AS fullname * FROM users * WHERE title = '' AND (status = 1 OR status = 0) * LIMIT 5 */ -$results = $res->asAssoc()->results(); +$results = $res->asAssoc() + ->rows(); foreach ($results as $row) { echo $row['fullname']; } @@ -308,10 +295,22 @@ namespace App\Model; class Posts extends \InitPHP\Database\Model { + /** + * If your model will use a connection other than your global connection, provide connection information. + * @var array|null

Default : NULL

+ */ + protected array $credentials = [ + 'dsn' => '', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + ]; + /** * If not specified, \InitPHP\Database\Entity::class is used by default. * - * @var \InitPHP\Database\Entity|string + * @var \InitPHP\Database\Orm\Entity|string */ protected $entity = \App\Entities\PostEntity::class; @@ -320,14 +319,14 @@ class Posts extends \InitPHP\Database\Model * * @var string */ - protected string $table = 'post'; + protected string $schema = 'posts'; /** * The name of the PRIMARY KEY column. If not, define it as NULL. * * @var null|string */ - protected ?string $primaryKey = 'id'; + protected ?string $schemaId = 'id'; /** * Specify FALSE if you want the data to be permanently deleted. @@ -357,53 +356,6 @@ class Posts extends \InitPHP\Database\Model */ protected ?string $deletedField = 'deleted_at'; - /** - * An array that defines the columns that will be allowed to be used in Insert and Update operations. - * If you want to give access to all columns; You can specify it as NULL. - * - * @var null|string[] - */ - protected ?array $allowedFields = [ - 'title', 'content', // ... - ]; - - /** - * Turns the use of callable functions on or off. - * - * @var bool - */ - protected bool $allowedCallbacks = false; - - /** - * @var string[]|\Closure[] - */ - protected array $beforeInsert = []; - - /** - * @var string[]|\Closure[] - */ - protected array $afterInsert = []; - - /** - * @var string[]|\Closure[] - */ - protected array $beforeUpdate = []; - - /** - * @var string[]|\Closure[] - */ - protected array $afterUpdate = []; - - /** - * @var string[]|\Closure[] - */ - protected array $beforeDelete = []; - - /** - * @var string[]|\Closure[] - */ - protected array $afterDelete = []; - protected bool $readable = true; protected bool $writable = true; @@ -411,25 +363,6 @@ class Posts extends \InitPHP\Database\Model protected bool $deletable = true; protected bool $updatable = true; - - protected array $validation = [ - 'id' => ['is_unique', 'int'], - 'title' => ['required', 'string', 'length(0,255)'], - ]; - - protected array $validationMsg = [ - 'id' => [], - 'title' => [ - 'required' => '{field} cannot be left blank.', - 'string' => '{field} must be a string.', - ], - ]; - - protected array $validationLabels = [ - 'id' => 'Post ID', - 'title' => 'Post Title', - // ... - ]; } ``` @@ -439,7 +372,7 @@ The most basic example of a entity class would look like this. ```php namespace App\Entities; -class PostEntity extends \InitPHP\Database\Entity +class PostEntity extends \InitPHP\Database\ORM\Entity { /** * An example of a getter method for the "post_title" column. @@ -466,7 +399,7 @@ class PostEntity extends \InitPHP\Database\Entity } ``` -## Developement Tools +## Development Tools Below I have mentioned some developer tools that you can use during and after development. @@ -549,16 +482,16 @@ DB::createImmutable([ ### Profiler Mode -Profiler mode is a developer tool available in v2.2 and above. It is a feature that allows you to see the executed queries along with their execution times. +Profiler mode is a developer tool available in v3 and above. It is a feature that allows you to see the executed queries along with their execution times. ```php use InitPHP\Database\Facade\DB; -DB::enableQueryProfiler(); +DB::enableQueryLog(); DB::table('users')->where('name', 'John')->get(); -var_dump(DB::getProfilerQueries()); +var_dump(DB::getQueryLogs()); /** * The output of the above example looks like this; diff --git a/composer.json b/composer.json index 52151be..ffb1fd3 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "initphp/database", - "description": "InitPHP DB (QueryBuilder, DBAL and ORM) Library", + "description": "InitPHP DataBase (QueryBuilder, DBAL and ORM) Library", "type": "library", "license": "MIT", "autoload": { @@ -23,10 +23,10 @@ ], "minimum-stability": "stable", "require": { - "php": ">=7.4", + "php": ">=8.0", "ext-pdo": "*" }, "require-dev": { - "phpunit/phpunit": "9.5" + "phpunit/phpunit": "^10.4" } } diff --git a/src/Connection/Connection.php b/src/Connection/Connection.php new file mode 100644 index 0000000..233b8e1 --- /dev/null +++ b/src/Connection/Connection.php @@ -0,0 +1,146 @@ + '', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + + 'debug' => false, + 'log' => null, + ]; + + private ?PDO $pdo = null; + + private array $transaction = [ + 'status' => false, + 'enable' => false, + 'testMode' => false, + ]; + + public function __construct(?array $credentials = null) + { + !empty($credentials) && $this->credentials = array_merge($this->credentials, $credentials); + } + + /** + * @inheritDoc + */ + public function getCredentials(?string $key = null, mixed $default = null): mixed + { + if ($key === null) { + return $this->credentials; + } + + return $this->credentials[$key] ?? $default; + } + + /** + * @inheritDoc + */ + public function getPDO(): PDO + { + !isset($this->pdo) && $this->connect(); + + return $this->pdo; + } + + /** + * @inheritDoc + */ + public function connect(): bool + { + try { + $options = [ + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS, + ]; + + $this->pdo = new PDO($this->getCredentials('dsn'), $this->getCredentials('username'), $this->getCredentials('password'), $options); + + if ($charset = $this->getCredentials('charset')) { + if ($collation = $this->getCredentials('collation')) { + $this->pdo->exec("SET NAMES '" . $charset . "' COLLATE '" . $collation . "'"); + } + $this->pdo->exec("SET CHARACTER SET '" . $charset . "'"); + } + + return true; + } catch (Exception $e) { + throw new ConnectionException($e->getMessage(), (int)$e->getCode(), $e->getPrevious()); + } + } + + /** + * @inheritDoc + */ + public function beginTransaction(bool $testMode = false): bool + { + $this->transaction = [ + 'status' => true, + 'enable' => true, + 'testMode' => $testMode, + ]; + + return $this->getPDO()->beginTransaction(); + } + + /** + * @inheritDoc + */ + public function completeTransaction(): bool + { + return $this->transaction['status'] === false || $this->transaction['testMode'] === true ? $this->rollBack() : $this->commit(); + } + + /** + * @inheritDoc + */ + public function commit(): bool + { + $this->transaction = [ + 'status' => false, + 'enable' => false, + 'testMode' => false, + ]; + + return $this->getPDO()->commit(); + } + + /** + * @inheritDoc + */ + public function rollBack(): bool + { + $this->transaction = [ + 'status' => false, + 'enable' => false, + 'testMode' => false, + ]; + + return $this->getPDO()->rollBack(); + } + + /** + * @inheritDoc + */ + public function disconnect(): bool + { + $this->pdo = null; + + return true; + } + +} diff --git a/src/Connection/Exceptions/ConnectionException.php b/src/Connection/Exceptions/ConnectionException.php new file mode 100644 index 0000000..226baf2 --- /dev/null +++ b/src/Connection/Exceptions/ConnectionException.php @@ -0,0 +1,8 @@ +db = &$db; + } + + public function __call(string $name, array $arguments) + { + $res = $this->db->{$name}(...$arguments); + + return ($res instanceof DatabaseInterface) ? $this : $res; + } + + /** + * @inheritDoc + */ + public function create(?string $table = null, ?array $set = null): bool + { + $builder = $this->db->getQueryBuilder(); + + !empty($set) && $builder->set($set); + + !empty($table) && $builder->from($table); + + $res = $this->db->query($builder->generateInsertQuery()); + $this->db->getQueryBuilder()->getParameter()->reset(); + + return $res->numRows() > 0; + } + + /** + * @inheritDoc + */ + public function createBatch(?string $table = null, ?array $set = null): bool + { + $builder = $this->db->getQueryBuilder(); + + if (!empty($set)) { + foreach ($set as $row) { + !empty($row) && is_array($row) + && $builder->set($row); + } + } + + !empty($table) && $builder->from($table); + + $res = $this->db->query($builder->generateBatchInsertQuery()); + $this->db->getQueryBuilder()->resetStructure(); + + return $res->numRows() > 0; + } + + /** + * @inheritDoc + */ + public function read(?string $table = null, array $selector = [], array $conditions = [], array $parameters = []): ResultInterface + { + $builder = $this->db->getQueryBuilder(); + + !empty($parameters) && $builder->getParameter()->merge($parameters); + + !empty($table) && $builder->from($table); + + + $res = $this->db->query($builder->generateSelectQuery($selector, $conditions)); + $this->db->getQueryBuilder()->resetStructure(); + + return $res; + } + + /** + * @inheritDoc + */ + public function readOne(?string $table = null, array $selector = [], array $conditions = [], array $parameters = []): ResultInterface + { + $builder = $this->db->getQueryBuilder(); + + !empty($parameters) && $builder->getParameter()->merge($parameters); + + !empty($table) && $builder->from($table); + + $res = $this->db->query($builder->limit(1) + ->generateSelectQuery($selector, $conditions)); + + $this->db->getQueryBuilder()->resetStructure(); + + return $res; + } + + /** + * @inheritDoc + */ + public function update(?string $table = null, ?array $set = null): bool + { + $builder = $this->db->getQueryBuilder(); + + !empty($set) && $builder->set($set); + !empty($table) && $builder->from($table); + + $res = $this->db->query($builder + ->generateUpdateQuery()); + + $this->db->getQueryBuilder()->resetStructure(); + + return $res->numRows() > 0; + } + + /** + * @inheritDoc + */ + public function updateBatch(?string $table = null, ?array $set = null, ?string $referenceColumn = null): bool + { + $builder = $this->db->getQueryBuilder(); + + if (!empty($set)) { + foreach ($set as $row) { + if (!empty($row) && is_array($row)) { + $builder->set($row); + } + } + } + !empty($table) && $builder->from($table); + + $res = $this->db->query($builder + ->generateUpdateBatchQuery($referenceColumn)); + + $this->db->getQueryBuilder()->resetStructure(); + + return $res->numRows() > 0; + } + + /** + * @inheritDoc + */ + public function delete(?string $table = null, ?array $conditions = []): bool + { + $builder = $this->db->getQueryBuilder(); + + if (!empty($conditions)) { + foreach ($conditions as $column => $value) { + if (is_string($column)) { + $builder->where($column, $value); + } else { + $builder->where($value); + } + } + } + !empty($table) && $builder->from($table); + + $res = $this->db->query($builder->generateDeleteQuery()); + + return $res->numRows() > 0; + } + +} diff --git a/src/DBAL/Database.php b/src/DBAL/Database.php new file mode 100644 index 0000000..22b6b5c --- /dev/null +++ b/src/DBAL/Database.php @@ -0,0 +1,292 @@ + true, + 'queryLog' => false, + ]; + + private ResultInterface $lastResult; + + private array $queryLogs = []; + + private array $errors = []; + + public function __construct(ConnectionInterface $connection, QueryBuilderInterface $builder) + { + $this->connection = $connection; + $this->builder = $builder; + $this->queryOptions = array_merge($this->queryOptions, $connection->getCredentials()); + } + + /** + * @throws Exception + */ + public function __call(string $name, array $arguments) + { + if (method_exists($this->connection, $name)) { + $res = $this->connection->{$name}(...$arguments); + + return ($res instanceof ConnectionInterface) ? $this : $res; + } + + if (method_exists($this->builder, $name)) { + $res = $this->connection->{$name}(...$arguments); + + return ($res instanceof QueryBuilderInterface) ? $this : $res; + } + + throw new Exception("There is no method called \"" . $name . "\""); + } + + /** + * @inheritDoc + */ + public function getErrors(): array + { + return $this->errors ?? []; + } + + /** + * @inheritDoc + */ + public function getQueryBuilder(): QueryBuilderInterface + { + return $this->builder; + } + + /** + * @inheritDoc + */ + public function getConnection(): ConnectionInterface + { + return $this->connection; + } + + /** + * @inheritDoc + */ + public function builder(): QueryBuilderInterface + { + return $this->getQueryBuilder()->newBuilder(); + } + + /** + * @inheritDoc + */ + public function newInstance(ConnectionInterface|array $connectionOrCredentials, ?QueryBuilderInterface $builder = null): self + { + return new self(($connectionOrCredentials instanceof ConnectionInterface) ? $connectionOrCredentials : new Connection($connectionOrCredentials), $builder ?? $this->builder); + } + + /** + * @inheritDoc + */ + public function get(?string $table = null, ?array $selection = null, ?array $conditions = null): ResultInterface + { + !empty($table) && $this->getQueryBuilder()->addFrom($table); + $res = $this->query($this->getQueryBuilder()->generateSelectQuery($selection ?? [], $conditions ?? [])); + $this->getQueryBuilder() + ->resetStructure(); + + return $res; + } + + /** + * @inheritDoc + */ + public function query(string $rawSQL, ?array $arguments = null, ?array $options = null): ResultInterface + { + $arguments = array_merge($this->getQueryBuilder()->getParameter()->all(), ($arguments ?? [])); + $options = array_merge($this->queryOptions, ($options ?? [])); + + try { + $timerStart = $options['profiler'] ? microtime(true) : 0; + $stmt = $this->getConnection()->getPDO()->prepare($rawSQL); + if ($stmt === false) { + throw new SQLQueryExecuteException('The SQL query could not be prepared.'); + } + if (!empty($arguments)) { + foreach ($arguments as $key => $value) { + $stmt->bindValue($key, $value, $this->__getValueBindType($value)); + } + } + if ($options['parameterReset']) { + $this->getQueryBuilder() + ->getParameter() + ->reset(); + } + $execute = $stmt->execute(); + if ($options['queryLog']) { + $timer = round((microtime(true) - $timerStart), 5); + $this->queryLogs[] = [ + 'query' => $rawSQL, + 'time' => $timer, + 'args' => $arguments, + ]; + } + if ($execute === false) { + throw new SQLQueryExecuteException('The SQL query could not be executed.'); + } + $errorCode = $stmt->errorCode(); + if($errorCode !== null && !empty(trim($errorCode, "0 \t\n\r\0\x0B"))){ + $errorInfo = $stmt->errorInfo(); + if(isset($errorInfo[2])){ + $msg = $errorCode . ' - ' . $errorInfo[2]; + $this->errors[] = $msg; + !empty($options['log']) && $this->createLog(($msg . ' SQL : ' . $rawSQL), $options['log']); + } + } + $this->lastResult = new Result($stmt); + + return !empty($options['fetch_mode']) + ? $this->lastResult->setFetchMode($options['fetch_mode']) + : $this->lastResult; + + } catch (Exception $e) { + $message = $e->getMessage(); + $sqlQuery = empty($arguments) ? $rawSQL : strtr($rawSQL, $arguments); + !empty($options['log']) && $this->createLog(($message . ' SQL : ' . $sqlQuery), $options['log']); + if ($options['debug'] === true) { + $message .= ' SQL : ' . $sqlQuery; + } + if ($options['parameterReset']) { + $this->getQueryBuilder() + ->getParameter() + ->reset(); + } + throw new SQLQueryExecuteException($message, (int)$e->getCode()); + } + } + + /** + * @inheritDoc + */ + public function count(): int + { + $builder = $this->getQueryBuilder()->clone() + ->resetStructure('select', false); + + $builder->selectCount('*', 'row_count'); + + $res = $this->query($builder->generateSelectQuery(), null, [ + 'parameterReset' => false, + ]); + + return $res->asAssoc()->row()['row_count'] ?? 0; + } + + /** + * @inheritDoc + */ + public function insertId(): int + { + $id = $this->getConnection()->getPDO()->lastInsertId(); + + return $id === false ? 0 : (int)$id; + } + + + /** + * @inheritDoc + */ + public function enableQueryLog(): DatabaseInterface + { + $this->queryOptions['queryLog'] = true; + + return $this; + } + + /** + * @inheritDoc + */ + public function disableQueryLog(): DatabaseInterface + { + $this->queryOptions['queryLog'] = false; + + return $this; + } + + /** + * @inheritDoc + */ + public function getQueryLogs(): array + { + return $this->queryLogs; + } + + /** + * @inheritDoc + */ + public function transaction(Closure $closure, int $attempt = 1, bool $testMode = false): bool + { + $attempt < 1 && $attempt = 1; + $res = false; + for ($i = 0; $i < $attempt; ++$i) { + try { + $this->getConnection()->beginTransaction($testMode); + call_user_func_array($closure, [$this]); + $res = $testMode ? $this->getConnection()->rollBack() : $this->getConnection()->commit(); + break; + } catch (Throwable $e) { + $res = $this->getConnection()->rollBack(); + $this->createLog('Transaction Rollback : ' . $e->getMessage()); + } + } + + return $res; + } + + protected function __getValueBindType($value): int + { + return match (true) { + is_bool($value), is_int($value) => PDO::PARAM_INT, + is_null($value) => PDO::PARAM_NULL, + default => PDO::PARAM_STR, + }; + } + + private function createLog(string $message, mixed $handler = null): void + { + $handler === null && $handler = $this->getConnection()->getCredentials('log'); + if (empty($handler)) { + return; + } + if (is_callable($handler)) { + call_user_func_array($handler, [$message]); + } else if (is_object($handler) && method_exists($handler, 'critical')) { + $handler->critical($message); + } else if (is_string($handler)) { + $path = strtr($handler, [ + '{timestamp}' => time(), + '{date}' => date("Y-m-d"), + '{year}' => date("Y"), + '{month}' => date("m"), + '{day}' => date("d"), + '{hour}' => date("H"), + '{minute}' => date("i"), + '{second}' => date("s"), + ]); + @file_put_contents($path, $message, FILE_APPEND); + } + + } + +} diff --git a/src/DBAL/Exceptions/SQLQueryExecuteException.php b/src/DBAL/Exceptions/SQLQueryExecuteException.php new file mode 100644 index 0000000..c80352a --- /dev/null +++ b/src/DBAL/Exceptions/SQLQueryExecuteException.php @@ -0,0 +1,9 @@ +\PDO::FETCH_*

+ * @return self + */ + public function setFetchMode(int $mode): self; + + /** + * @param string $class + * @return self + */ + public function asClass(string $class = Entity::class): self; + + /** + * @return self + */ + public function asObject(): self; + + + /** + * @return self + */ + public function asAssoc(): self; + + /** + * @return self + */ + public function asArray(): self; + + /** + * @return self + */ + public function asLazy(): self; + + /** + * @return array|object|false + */ + public function row(): array|object|false; + + /** + * @return array[]|object[]|false + */ + public function rows(): array|false; + + +} \ No newline at end of file diff --git a/src/DBAL/Result.php b/src/DBAL/Result.php new file mode 100644 index 0000000..cf2febe --- /dev/null +++ b/src/DBAL/Result.php @@ -0,0 +1,144 @@ +setStatement($statement); + } + + /** + * @inheritDoc + */ + public function setStatement(PDOStatement $statement): self + { + $this->statement = $statement; + $this->query = $statement->queryString; + $this->numRows = $statement->rowCount(); + + return $this; + } + + /** + * @inheritDoc + */ + public function withStatement(PDOStatement $statement): self + { + return new self($statement); + } + + /** + * @inheritDoc + */ + public function getStatement(): PDOStatement + { + return $this->statement; + } + + /** + * @inheritDoc + */ + public function numRows(): int + { + return $this->numRows; + } + + /** + * @inheritDoc + */ + public function query(): string + { + return $this->query; + } + + /** + * @inheritDoc + */ + public function setFetchMode(int $mode): self + { + $this->getStatement()->setFetchMode($mode); + + return $this; + } + + /** + * @inheritDoc + */ + public function asClass(string $class = Entity::class): self + { + $this->getStatement()->setFetchMode(PDO::FETCH_CLASS, $class); + + return $this; + } + + /** + * @inheritDoc + */ + public function asObject(): self + { + $this->getStatement()->setFetchMode(PDO::FETCH_OBJ); + + return $this; + } + + /** + * @inheritDoc + */ + public function asAssoc(): self + { + $this->getStatement()->setFetchMode(PDO::FETCH_ASSOC); + + return $this; + } + + /** + * @inheritDoc + */ + public function asArray(): self + { + $this->getStatement()->setFetchMode(PDO::FETCH_BOTH); + + return $this; + } + + /** + * @inheritDoc + */ + public function asLazy(): self + { + $this->getStatement()->setFetchMode(PDO::FETCH_LAZY); + + return $this; + } + + /** + * @inheritDoc + */ + public function row(): array|object|false + { + return $this->getStatement()->fetch(); + } + + /** + * @inheritDoc + */ + public function rows(): array|false + { + return $this->getStatement()->fetchAll(); + } + +} diff --git a/src/Database.php b/src/Database.php deleted file mode 100644 index 52a1440..0000000 --- a/src/Database.php +++ /dev/null @@ -1,844 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.1 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database; - -use InitPHP\Database\Utils\{Pagination, - Datatables}; -use InitPHP\Database\Exceptions\{QueryBuilderException, - QueryGeneratorException, - WritableException, - ReadableException, - UpdatableException, - DeletableException, - SQLQueryExecuteException, - ConnectionException}; -use \InitPHP\Database\Helpers\{Helper, Parameters, Validation}; -use \PDO; -use \RuntimeException; -use \PDOException; -use \Exception; -use \InvalidArgumentException; -use \Closure; - -use function microtime; -use function round; -use function count; -use function current; -use function substr; -use function array_merge; -use function is_numeric; -use function is_bool; -use function is_string; -use function is_int; -use function is_null; -use function is_callable; -use function is_object; -use function is_array; -use function in_array; -use function trim; -use function ltrim; -use function stripslashes; -use function serialize; -use function strtr; -use function str_replace; -use function strtoupper; -use function call_user_func_array; -use function file_put_contents; -use function time; -use function date; -use function method_exists; - -use const COUNT_RECURSIVE; -use const FILE_APPEND; - -class Database extends QueryBuilder -{ - - public const ENTITY = 0; - public const ASSOC = 1; - public const ARRAY = 2; - public const OBJECT = 3; - public const LAZY = 4; - - private PDO $_pdo; - - private bool $_isGlobal = false; - - private static PDO $_globalPDO; - - private array $_credentials = [ - 'dsn' => '', - 'username' => 'root', - 'password' => null, - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'tableSchema' => null, - 'tableSchemaID' => null, - 'entity' => Entity::class, - 'createdField' => null, - 'updatedField' => null, - 'deletedField' => null, - 'allowedFields' => null, - 'timestampFormat' => 'c', - 'validation' => [ - 'methods' => [], - 'messages' => [], - 'labels' => [], - ], - 'readable' => true, - 'writable' => true, - 'deletable' => true, - 'updatable' => true, - 'return' => null, - 'debug' => false, - 'log' => null, - 'profiler' => false, - ]; - - private Result $_last; - - private array $_transaction = [ - 'status' => false, - 'enable' => false, - 'testMode' => false, - ]; - - private array $_errors = []; - - private array $_queryLogs = []; - - private Validation $_validation; - - public function __construct(array $credentials = []) - { - $this->setCredentials($credentials); - $this->_validation = new Validation($this->_credentials['validation']['methods'], $this->_credentials['validation']['messages'], $this->_credentials['validation']['labels'], $this); - } - - public function __call($name, $arguments) - { - if(Helper::str_starts_with($name, 'findBy') === FALSE){ - throw new RuntimeException('There is no "' . $name . '" method.'); - } - $this->where(Helper::camelCaseToSnakeCase(substr($name, 6)), current($arguments)); - - return $this; - } - - final public function newInstance(array $credentials = []): Database - { - $instance = new Database(empty($credentials) ? $this->_credentials : array_merge($this->_credentials, $credentials)); - $instance->_isGlobal = false; - - return $instance; - } - - final public function clone(): Database - { - $clone = new self($this->_credentials); - $clone->_isGlobal = $this->_isGlobal; - - return $clone; - } - - final public function enableQueryProfiler(): void - { - $this->_credentials['profiler'] = true; - } - - final public function disableQueryProfiler(): void - { - $this->_credentials['profiler'] = false; - } - - final public function getProfilerQueries(): array - { - return $this->_queryLogs; - } - - final public function setCredentials(array $credentials): self - { - $this->_credentials = array_merge($this->_credentials, $credentials); - return $this; - } - - final public function getCredentials(?string $key = null) - { - return ($key === null) ? $this->_credentials : ($this->_credentials[$key] ?? null); - } - - final public function isError(): bool - { - return !empty($this->_errors); - } - - final public function getError(): array - { - return $this->_errors; - } - - final public function connectionAsGlobal(): void - { - self::$_globalPDO = $this->getPDO(); - $this->_isGlobal = true; - } - - public function getPDO(): PDO - { - if(isset(self::$_globalPDO) && $this->_isGlobal){ - return self::$_globalPDO; - } - if(!isset($this->_pdo)){ - try { - $options = [ - PDO::ATTR_EMULATE_PREPARES => false, - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - ]; - switch ($this->_credentials['return']) { - case null: - case self::ARRAY: - $options[PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_BOTH; - break; - case self::ASSOC: - $options[PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_ASSOC; - break; - case self::ENTITY: - $options[PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_CLASS; - break; - case self::OBJECT: - $options[PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_OBJ; - break; - case self::LAZY: - $options[PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_LAZY; - break; - default: - $options[PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_BOTH; - } - - $this->_pdo = new PDO($this->_credentials['dsn'], $this->_credentials['username'], $this->_credentials['password'], $options); - if(!empty($this->_credentials['charset'])){ - if(!empty($this->_credentials['collation'])) { - $this->_pdo->exec("SET NAMES '" . $this->_credentials['charset'] . "' COLLATE '" . $this->_credentials['collation'] . "'"); - } - $this->_pdo->exec("SET CHARACTER SET '" . $this->_credentials['charset'] . "'"); - } - } catch (PDOException $e) { - throw new ConnectionException($e->getMessage(), (int)$e->getCode()); - } - } - - return $this->_pdo; - } - - final public function withPDO(PDO $pdo): self - { - $with = clone $this; - $with->_pdo = $pdo; - $with->_isGlobal = false; - - return $with; - } - - final public function getSchemaID(): ?string - { - return $this->_credentials['tableSchemaID']; - } - - final public function setSchemaID(?string $column): self - { - $this->_credentials['tableSchemaID'] = $column; - - return $this; - } - - final public function withSchemaID(?string $column): self - { - $with = clone $this; - - return $with->setSchemaID($column); - } - - final public function getSchema(): ?string - { - return $this->_credentials['tableSchema']; - } - - final public function setSchema(?string $table): self - { - $this->_credentials['tableSchema'] = $table; - - return $this; - } - - final public function withSchema(?string $table): self - { - $with = clone $this; - - return $with->setSchema($table); - } - - final public function beginTransaction(bool $testMode = false): bool - { - $this->_transaction = [ - 'status' => true, - 'enable' => true, - 'testMode' => $testMode, - ]; - - return (bool)$this->getPDO()->beginTransaction(); - } - - final public function completeTransaction(): bool - { - return ($this->_transaction['status'] === FALSE || $this->_transaction['testMode'] === TRUE) ? $this->rollBack() : $this->commit(); - } - - final public function commit(): bool - { - $this->_transaction = [ - 'status' => false, - 'enable' => false, - 'testMode' => false, - ]; - - return (bool)$this->getPDO()->commit(); - } - - final public function rollBack(): bool - { - $this->_transaction = [ - 'status' => false, - 'enable' => false, - 'testMode' => false, - ]; - - return (bool)$this->getPDO()->rollBack(); - } - - final public function transaction(Closure $closure): bool - { - try { - $this->beginTransaction(false); - call_user_func_array($closure, [$this]); - $res = $this->completeTransaction(); - } catch (Exception $e) { - $res = $this->rollBack(); - } - - return $res; - } - - final public function connection(array $credentials = []): self - { - return $this->newInstance($credentials); - } - - /** - * @param $value - * @return false|float|int|string - */ - final public function escape_str($value) - { - if(is_numeric($value)){ - return $value; - } - if(is_bool($value)){ - return $value === FALSE ? 'FALSE' : 'TRUE'; - } - if($value === null){ - return 'NULL'; - } - if(is_string($value)){ - $value = str_replace("\\", "", trim(stripslashes($value), "' \\\t\n\r\0\x0B")); - return "'" . str_replace("'", "\\'", $value) . "'"; - } - if(is_object($value)){ - return serialize($value); - } - if(is_array($value)){ - return serialize($value); - } - return false; - } - - /** - * @param string $sqlQuery - * @param array $parameters - * @return Result - */ - final public function query(string $sqlQuery, array $parameters = []): Result - { - $arguments = Parameters::get(true); - $parameters = empty($parameters) ? $arguments : array_merge($arguments, $parameters); - try { - $timerStart = $this->_credentials['profiler'] ? microtime(true) : 0; - $stmt = $this->getPDO()->prepare($sqlQuery); - if($stmt === FALSE){ - throw new Exception('The SQL query could not be prepared.'); - } - if(!empty($parameters)){ - foreach ($parameters as $key => $value) { - $stmt->bindValue(':' . ltrim($key, ':'), $value, $this->_bind($value)); - } - } - $execute = $stmt->execute(); - if ($this->_credentials['profiler']) { - $timer = round((microtime(true) - $timerStart), 5); - $this->_queryLogs[] = [ - 'query' => $sqlQuery, - 'time' => $timer, - 'args' => $parameters, - ]; - } - if($execute === FALSE){ - throw new Exception('The SQL query could not be executed.'); - } - $errorCode = $stmt->errorCode(); - if($errorCode !== null && !empty(trim($errorCode, "0 \t\n\r\0\x0B"))){ - $errorInfo = $stmt->errorInfo(); - if(isset($errorInfo[2])){ - $this->_errors[] = $errorCode . ' - ' . $errorInfo[2]; - } - } - $this->_last = new Result($stmt); - if($this->_credentials['return'] === null){ - return $this->_last; - } - switch ($this->_credentials['return']) { - case self::ASSOC: - return $this->_last->asAssoc(); - case self::ARRAY: - return $this->_last->asArray(); - case self::ENTITY: - return $this->_last->asEntity($this->_credentials['entity'] ?? Entity::class); - case self::OBJECT: - return $this->_last->asObject(); - case self::LAZY: - return $this->_last->asLazy(); - default: - return $this->_last; - } - } catch (Exception $e) { - $message = $e->getMessage(); - $sqlMessage = 'SQL : ' - . (empty($parameters) ? $sqlQuery : strtr($sqlQuery, $parameters)); - if(!empty($this->_credentials['log'])){ - $this->_logCreate($message . ' ' . $sqlMessage); - } - if($this->_credentials['debug'] === TRUE){ - $message .= $message . ' ' . $sqlMessage; - } - throw new SQLQueryExecuteException($message, (int)$e->getCode()); - } - } - - /** - * @return int - */ - final public function insertId(): int - { - $id = $this->getPDO()->lastInsertId(); - - return $id === FALSE ? 0 : (int)$id; - } - - /** - * @param string|null $table - * @return Result - * @throws QueryGeneratorException - */ - final public function get(?string $table = null): Result - { - if(!empty($table)){ - $this->addFrom($table); - } - $res = $this->query($this->generateSelectQuery()); - $this->reset(); - - return $res; - } - - /** - * @param array|null $set - * @return bool - * @throws QueryBuilderException - * @throws QueryGeneratorException - */ - public function create(?array $set = null) - { - if($this->_credentials['writable'] === FALSE){ - throw new WritableException(''); - } - - if($set !== null && count($set) === count($set, COUNT_RECURSIVE)){ - $this->_validation->setData($set); - foreach ($set as $column => $value) { - if($this->_validation->validation($column, null) === FALSE){ - $this->_errors[] = $this->_validation->getError(); - return false; - } - $this->set($column, $value); - } - } - - $res = $this->query($this->generateInsertQuery()); - $this->reset(); - - return $res->numRows() > 0; - } - - /** - * @param array|null $set - * @return bool - * @throws QueryBuilderException - * @throws QueryGeneratorException - */ - public function insert(?array $set = null) - { - return $this->create($set); - } - - /** - * @param array|null $set - * @return bool - * @throws QueryBuilderException - * @throws QueryGeneratorException - */ - public function createBatch(?array $set = null) - { - if($this->_credentials['writable'] === FALSE){ - throw new WritableException(''); - } - - if ($set !== null && count($set) !== count($set, COUNT_RECURSIVE)) { - foreach ($set as $row) { - $data = []; - $this->_validation->setData($row); - foreach ($row as $column => $value) { - if ($this->_validation->validation($column, null) === FALSE) { - $this->_errors[] = $this->_validation->getError(); - return false; - } - $data[$column] = $value; - } - if (empty($data)) { - continue; - } - $this->set($data); - } - } - - $res = $this->query($this->generateBatchInsertQuery()); - $this->reset(); - - return $res->numRows() > 0; - } - - - /** - * @param array $set - * @return bool - * @throws QueryBuilderException - * @throws QueryGeneratorException - */ - public function insertBatch(array $set) - { - return $this->createBatch($set); - } - - /** - * @param array $selector - * @param array $conditions - * @param array $parameters - * @return Result - */ - public function read(array $selector = [], array $conditions = [], array $parameters = []) - { - if($this->_credentials['readable'] === FALSE){ - throw new ReadableException(''); - } - Parameters::merge($parameters); - $query = $this->generateSelectQuery($selector, $conditions); - - $res = $this->query($query); - $this->reset(); - - return $res; - } - - /** - * @param array $selector - * @param array $conditions - * @param array $parameters - * @return Result - * @throws QueryGeneratorException - */ - public function readOne(array $selector = [], array $conditions = [], array $parameters = []) - { - if($this->_credentials['readable'] === FALSE){ - throw new ReadableException(''); - } - Parameters::merge($parameters); - $query = $this->limit(1) - ->generateSelectQuery($selector, $conditions); - - $res = $this->query($query); - $this->reset(); - - return $res; - } - - /** - * @param array|null $set - * @return bool - * @throws QueryBuilderException - * @throws QueryGeneratorException - */ - public function update(?array $set = null) - { - if($this->_credentials['updatable'] === FALSE){ - throw new UpdatableException(''); - } - if ($set !== null) { - $schemaID = $this->getSchemaID(); - $this->_validation->setData($set); - foreach ($set as $column => $value) { - if ($this->_validation->validation($column, $schemaID) === FALSE) { - $this->_errors[] = $this->_validation->getError(); - return false; - } - } - $this->set($set); - } - - $res = $this->query($this->generateUpdateQuery()); - $this->reset(); - - return $res->numRows() > 0; - } - - /** - * @param string $referenceColumn - * @param array|null $set - * @return bool - * @throws Exceptions\QueryBuilderException - * @throws Exceptions\QueryGeneratorException - */ - public function updateBatch(string $referenceColumn, ?array $set = null) - { - if($this->_credentials['updatable'] === FALSE){ - throw new UpdatableException(''); - } - - if ($set !== null) { - foreach ($set as $data) { - $this->_validation->setData($data); - foreach ($data as $column => $value) { - if (in_array($column, [$this->getSchemaID(), $referenceColumn])) { - continue; - } - if ($this->_validation->validation($column, $this->getSchemaID()) === FALSE) { - $this->_errors[] = $this->_validation->getError(); - return false; - } - } - $this->set($data); - } - } - - $res = $this->query($this->generateUpdateBatchQuery($referenceColumn)); - $this->reset(); - - return $res->numRows() > 0; - } - - - /** - * @param array $conditions - * @return bool - * @throws QueryBuilderException - * @throws QueryGeneratorException - */ - public function delete(array $conditions = []) - { - if($this->_credentials['deletable'] === FALSE){ - throw new DeletableException(''); - } - foreach ($conditions as $column => $value) { - if (is_string($column)) { - $this->where($column, $value); - } else { - $this->where($value); - } - } - if(!empty($this->_credentials['deletedField'])){ - if($this->isOnlyDeletes !== FALSE){ - $this->isNot($this->_credentials['deletedField'], null); - $query = $this->generateDeleteQuery(); - $this->isOnlyDeletes = false; - }else{ - $this->is($this->_credentials['deletedField'], null) - ->set($this->_credentials['deletedField'], date($this->_credentials['timestampFormat']), false); - $query = $this->generateUpdateQuery(); - } - }else{ - $query = $this->generateDeleteQuery(); - } - $res = $this->query($query); - - $this->reset(); - - return $res->numRows() > 0; - } - - /** - * @param int $limit - * @param int $offset - * @return Result - */ - public function all(int $limit = 100, int $offset = 0): Result - { - return $this->limit($limit) - ->offset($offset) - ->read(); - } - - /** - * @return Result - * @throws QueryGeneratorException - */ - public function first() - { - return $this->readOne(); - } - - public function group(Closure $group, string $logical = 'AND'): self - { - $logical = str_replace(['&&', '||'], ['AND', 'OR'], strtoupper($logical)); - if(!in_array($logical, ['AND', 'OR'], true)){ - throw new InvalidArgumentException('Logical operator OR, AND, && or || it could be.'); - } - - $clone = clone $this; - $clone->reset(); - - call_user_func_array($group, [$clone]); - - if($where = $clone->__generateWhereQuery()){ - $this->_STRUCTURE['where'][$logical][] = '(' . $where . ')'; - } - - if($having = $clone->__generateHavingQuery()){ - $this->_STRUCTURE['having'][$logical][] = '(' . $having . ')'; - } - unset($clone); - return $this; - } - - public function onlyDeleted(): self - { - $this->isOnlyDeletes = true; - - return $this; - } - - public function onlyUndeleted(): self - { - $this->isOnlyDeletes = false; - - return $this; - } - - /** - * QueryBuilder resetlemeden SELECT cümlesi kurar ve satır sayısını döndürür. - * - * @return int - */ - public function count(): int - { - $select = $this->_STRUCTURE['select']; - $this->_STRUCTURE['select'] = ['COUNT(*) AS row_count']; - $this->__generateSoftDeleteQuery(false); - $parameters = Parameters::get(false); - $res = $this->query($this->generateSelectQuery()); - $count = $res->toArray()['row_count'] ?? 0; - unset($res); - Parameters::merge($parameters); - $this->_STRUCTURE['select'] = $select; - - return (int)$count; - } - - public function pagination(int $page = 1, int $per_page_limit = 10, string $link = '?page={page}'): Pagination - { - $total_row = $this->count(); - $this->offset(($page - 1) * $per_page_limit) - ->limit($per_page_limit); - $res = $this->query($this->generateSelectQuery()); - $this->reset(); - - return new Pagination($res, $page, $per_page_limit, $total_row, $link); - } - - public function datatables(array $columns, int $method = Datatables::GET_REQUEST): string - { - return (new Datatables($this, $columns, $method))->__toString(); - } - - final public function isAllowedFields(string $column): bool - { - return empty($this->_credentials['allowedFields']) || in_array($column, $this->_credentials['allowedFields']); - } - - protected function _logCreate(string $message): void - { - if(empty($this->_credentials['log'])){ - return; - } - if(is_callable($this->_credentials['log'])){ - call_user_func_array($this->_credentials['log'], [$message]); - return; - } - if(is_string($this->_credentials['log'])){ - $path = strtr($this->_credentials['log'], [ - '{timestamp}' => time(), - '{date}' => date("Y-m-d"), - '{year}' => date("Y"), - '{month}' => date("m"), - '{day}' => date("d"), - '{hour}' => date("H"), - '{minute}' => date("i"), - '{second}' => date("s"), - ]); - @file_put_contents($path, $message, FILE_APPEND); - return; - } - if(is_object($this->_credentials['log']) && method_exists($this->_credentials['log'], 'critical')){ - $this->_credentials['log']->critical($message); - } - } - - private function _bind($value) - { - if(is_bool($value) || is_int($value)){ - return PDO::PARAM_INT; - } - if(is_null($value)){ - return PDO::PARAM_NULL; - } - return PDO::PARAM_STR; - } - -} diff --git a/src/Exceptions/ConnectionException.php b/src/Exceptions/ConnectionException.php deleted file mode 100644 index fc7d56f..0000000 --- a/src/Exceptions/ConnectionException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class ConnectionException extends \RuntimeException -{ -} diff --git a/src/Exceptions/DeletableException.php b/src/Exceptions/DeletableException.php deleted file mode 100644 index fb08baf..0000000 --- a/src/Exceptions/DeletableException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class DeletableException extends \RuntimeException -{ -} diff --git a/src/Exceptions/ModelCallbacksException.php b/src/Exceptions/ModelCallbacksException.php deleted file mode 100644 index 778609e..0000000 --- a/src/Exceptions/ModelCallbacksException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class ModelCallbacksException extends ModelException -{ -} diff --git a/src/Exceptions/ModelException.php b/src/Exceptions/ModelException.php deleted file mode 100644 index 324fa40..0000000 --- a/src/Exceptions/ModelException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class ModelException extends \RuntimeException -{ -} diff --git a/src/Exceptions/ModelRelationsException.php b/src/Exceptions/ModelRelationsException.php deleted file mode 100644 index c6a610d..0000000 --- a/src/Exceptions/ModelRelationsException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class ModelRelationsException extends ModelException -{ -} diff --git a/src/Exceptions/QueryBuilderException.php b/src/Exceptions/QueryBuilderException.php deleted file mode 100644 index 00c39d4..0000000 --- a/src/Exceptions/QueryBuilderException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.8 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class QueryBuilderException extends \Exception -{ -} diff --git a/src/Exceptions/QueryGeneratorException.php b/src/Exceptions/QueryGeneratorException.php deleted file mode 100644 index 4edb6a2..0000000 --- a/src/Exceptions/QueryGeneratorException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.8 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class QueryGeneratorException extends QueryBuilderException -{ -} diff --git a/src/Exceptions/ReadableException.php b/src/Exceptions/ReadableException.php deleted file mode 100644 index 38eba70..0000000 --- a/src/Exceptions/ReadableException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class ReadableException extends \RuntimeException -{ -} diff --git a/src/Exceptions/SQLQueryExecuteException.php b/src/Exceptions/SQLQueryExecuteException.php deleted file mode 100644 index 83dc91e..0000000 --- a/src/Exceptions/SQLQueryExecuteException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class SQLQueryExecuteException extends \RuntimeException -{ -} diff --git a/src/Exceptions/UpdatableException.php b/src/Exceptions/UpdatableException.php deleted file mode 100644 index 9977ce6..0000000 --- a/src/Exceptions/UpdatableException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class UpdatableException extends \RuntimeException -{ -} diff --git a/src/Exceptions/ValidationException.php b/src/Exceptions/ValidationException.php deleted file mode 100644 index 15711a3..0000000 --- a/src/Exceptions/ValidationException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class ValidationException extends \RuntimeException -{ -} diff --git a/src/Exceptions/ValueException.php b/src/Exceptions/ValueException.php deleted file mode 100644 index a81775d..0000000 --- a/src/Exceptions/ValueException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.8 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class ValueException extends QueryBuilderException -{ -} diff --git a/src/Exceptions/WritableException.php b/src/Exceptions/WritableException.php deleted file mode 100644 index 97d5ca5..0000000 --- a/src/Exceptions/WritableException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Exceptions; - -class WritableException extends \RuntimeException -{ -} diff --git a/src/Facade/DB.php b/src/Facade/DB.php index 2039a48..224f668 100644 --- a/src/Facade/DB.php +++ b/src/Facade/DB.php @@ -7,144 +7,155 @@ * @author Muhammet ŞAFAK * @copyright Copyright © 2022 Muhammet ŞAFAK * @license ./LICENSE MIT - * @version 2.0.7 + * @version 3.0 * @link https://www.muhammetsafak.com.tr */ namespace InitPHP\Database\Facade; -use InitPHP\Database\{Database, Raw, Result, Utils\Pagination}; +use \InitPHP\Database\Connection\{Connection, + Interfaces\ConnectionInterface}; +use \InitPHP\Database\DBAL\{CRUD, + Database, + Interfaces\CRUDInterface, + Interfaces\ResultInterface}; +use \InitPHP\Database\QueryBuilder\{QueryBuilder, + RawQuery, + Interfaces\ParameterInterface, + Interfaces\QueryBuilderInterface}; +use PDO; +use Closure; /** - * @mixin Database - * @method static Database newInstance(array $credentials = []) - * @method static bool isError() - * @method static string[] getError() - * @method static \PDO getPDO() - * @method static Database withPDO(\PDO $pdo) - * @method static null|string getSchemaID() - * @method static Database setSchemaID(null|string $column) - * @method static Database withSchemaID(null|string $column) - * @method static null|string getSchema() - * @method static Database setSchema(null|string $table) - * @method static Database withSchema(null|string $table) + * @mixin CRUDInterface + * @method static mixed getCredentials(?string $key = null, mixed $default = null) + * @method static PDO getPDO() * @method static bool beginTransaction(bool $testMode = false) * @method static bool completeTransaction() * @method static bool commit() * @method static bool rollBack() - * @method static bool transaction(\Closure $closure) - * @method static Raw raw(string $rawQuery) - * @method static Database connection(array $credentials = []) - * @method static false|float|int|string escape_str(mixed $value) - * @method static Database setParameter(string $key, mixed $value) - * @method static Database setParameters(array $parameters = []) - * @method static Result query(string $sqlQuery, array $parameters = []) - * @method static int insertId() - * @method static Result get(?string $table = null) - * @method static bool create(array $set) - * @method static bool createBatch(array $set) - * @method static bool insert(array $set) - * @method static bool insertBatch(array $set) + * @method static bool disconnect() + * @method static CRUDInterface enableQueryLog() + * @method static CRUDInterface disableQueryLog() + * @method static CRUDInterface getQueryLogs() + * @method static QueryBuilderInterface getQueryBuilder() + * @method static ConnectionInterface getConnection() + * @method static QueryBuilderInterface builder() + * @method static CRUDInterface newInstance(ConnectionInterface|array $connectionOrCredentials, ?QueryBuilderInterface $builder = null) + * @method static ResultInterface get(?string $table = null, ?array $selection = null, ?array $conditions = null) + * @method static ResultInterface query(string $rawSQL, ?array $arguments = null, ?array $options = null) * @method static int count() - * @method static Pagination pagination(int $page = 1, int $per_page_limit = 10, string $link = '?page={page}') - * @method static Result read(array $selector = [], array $conditions = [], array $parameters = []) - * @method static Result readOne(array $selector = [], array $conditions = [], array $parameters = []) - * @method static bool update(array $set) - * @method static bool delete(array $conditions = []) - * @method static Result all(int $limit = 100, int $offset = 0) - * @method static Database onlyDeleted() - * @method static Database onlyUndeleted() - * @method static void reset() - * @method static Database select(string|Raw ...$columns) - * @method static Database selectCount(string $column, ?string $alias = null) - * @method static Database selectMax(string $column, ?string $alias = null) - * @method static Database selectMin(string $column, ?string $alias = null) - * @method static Database selectAvg(string $column, ?string $alias = null) - * @method static Database selectAs(string $column, ?string $alias = null) - * @method static Database selectUpper(string $column, ?string $alias = null) - * @method static Database selectLower(string $column, ?string $alias = null) - * @method static Database selectLength(string $column, ?string $alias = null) - * @method static Database selectMid(string $column, int $offset, int $length, ?string $alias = null) - * @method static Database selectLeft(string $column, int $length, ?string $alias = null) - * @method static Database selectRight(string $column, int $length, ?string $alias = null) - * @method static Database selectDistinct(string $column, ?string $alias = null) - * @method static Database selectCoalesce(string $column, $default = '0', ?string $alias = null) - * @method static Database selectSum(string $column, ?string $alias = null) - * @method static Database selectConcat(?string $alias = null, string ...$columnOrStr) - * @method static Database from(string|Raw ...$tables) - * @method static Database table(string|Raw $table) - * @method static Database groupBy(string|Raw ...$columns) - * @method static Database join(string|Raw $table, ?string $onStmt = null, string $type = 'INNER') - * @method static Database selfJoin(string $table, string $onStmt) - * @method static Database innerJoin(string $table, string $onStmt) - * @method static Database leftJoin(string $table, string $onStmt) - * @method static Database rightJoin(string $table, string $onStmt) - * @method static Database leftOuterJoin(string $table, string $onStmt) - * @method static Database rightOuterJoin(string $table, string $onStmt) - * @method static Database naturalJoin(string $table) - * @method static Database orderBy(string $column, string $soft = 'ASC') - * @method static Database where(string|Raw $column, mixed $value = null, string $mark = '=', string $logical = 'AND') - * @method static Database having(string|Raw $column, mixed $value, string $mark = '=', string $logical = 'AND') - * @method static Database andWhere(string|Raw $column, $value, string $mark = '=') - * @method static Database orWhere(string|Raw $column, $value, string $mark = '=') - * @method static Database between(string $column, array $values, string $logical = 'AND') - * @method static Database orBetween(string $column, array $values) - * @method static Database andBetween(string $column, array $values) - * @method static Database notBetween(string $column, array $values, string $logical = 'AND') - * @method static Database orNotBetween(string $column, array $values) - * @method static Database andNotBetween(string $column, array $values) - * @method static Database findInSet(string $column, $value, string $logical = 'AND') - * @method static Database orFindInSet(string $column, $value) - * @method static Database andFindInSet(string $column, $value) - * @method static Database notFindInSet(string $column, $value, string $logical = 'AND') - * @method static Database andNotFindInSet(string $column, $value) - * @method static Database orNotFindInSet(string $column, $value) - * @method static Database in(string $column, array $value, string $logical = 'AND') - * @method static Database orIn(string $column, array $value) - * @method static Database andIn(string $column, array $value) - * @method static Database notIn(string $column, array $value, string $logical = 'AND') - * @method static Database orNotIn(string $column, array $value) - * @method static Database andNotIn(string $column, array $value) - * @method static Database regexp(string $column, string $value, string $logical = 'AND') - * @method static Database andRegexp(string $column, string $value) - * @method static Database orRegexp(string $column, string $value) - * @method static Database like(string $column, $value, string $logical = 'AND') - * @method static Database orLike(string $column, $value) - * @method static Database andLike(string $column, $value) - * @method static Database startLike(string $column, $value, string $logical = 'AND') - * @method static Database orStartLike(string $column, $value) - * @method static Database andStartLike(string $column, $value) - * @method static Database endLike(string $column, $value, string $logical = 'AND') - * @method static Database orEndLike(string $column, $value) - * @method static Database andEndLike(string $column, $value) - * @method static Database notLike(string $column, $value, string $logical = 'AND') - * @method static Database orNotLike(string $column, $value) - * @method static Database andNotLike(string $column, $value) - * @method static Database startNotLike(string $column, $value, string $logical = 'AND') - * @method static Database orStartNotLike(string $column, $value) - * @method static Database andStartNotLike(string $column, $value) - * @method static Database endNotLike(string $column, $value, string $logical = 'AND') - * @method static Database orEndNotLike(string $column, $value) - * @method static Database andEndNotLike(string $column, $value) - * @method static Database soundex(string $column, $value, string $logical = 'AND') - * @method static Database orSoundex(string $column, $value) - * @method static Database andSoundex(string $column, $value) - * @method static Database is(string $column, $value, string $logical = 'AND') - * @method static Database orIs(string $column, $value = null) - * @method static Database andIs(string $column, $value = null) - * @method static Database isNot(string $column, $value = null, string $logical = 'AND') - * @method static Database orIsNot(string $column, $value = null) - * @method static Database andIsNot(string $column, $value = null) - * @method static Database offset(int $offset = 0) - * @method static Database limit(int $limit) - * @method static void enableQueryProfiler() - * @method static void disableQueryProfiler() - * @method static array getProfilerQueries() + * @method static int insertId() + * @method static bool transaction(Closure $closure, int $attempt = 1, bool $testMode = false) + * @method static bool create(?string $table = null, ?array $set = null) + * @method static bool createBatch(?string $table = null, ?array $set = null) + * @method static ResultInterface read(?string $table = null, array $selector = [], array $conditions = [], array $parameters = []) + * @method static ResultInterface readOne(?string $table = null, array $selector = [], array $conditions = [], array $parameters = []) + * @method static bool update(?string $table = null, ?array $set = null) + * @method static bool updateBatch(?string $table = null, ?array $set = null, ?string $referenceColumn = null) + * @method static bool delete(?string $table = null, ?array $conditions = []) + * @method static CRUDInterface newBuilder() + * @method static CRUDInterface importQB(array $structure, bool $merge = false) + * @method static array exportQB() + * @method static ParameterInterface getParameter() + * @method static CRUDInterface setParameter(string $key, mixed $value) + * @method static CRUDInterface setParameters(array $parameters = []) + * @method static CRUDInterface select(string|RawQuery|string[]|RawQuery[] ...$columns) + * @method static CRUDInterface selectCount(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectCountDistinct(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectMax(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectMin(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectAvg(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectAs(RawQuery|string $column, string $alias) + * @method static CRUDInterface selectUpper(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectLower(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectLength(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectMid(RawQuery|string $column, int $offset, int $length, ?string $alias = null) + * @method static CRUDInterface selectLeft(RawQuery|string $column, int $length, ?string $alias = null) + * @method static CRUDInterface selectRight(RawQuery|string $column, int $length, ?string $alias = null) + * @method static CRUDInterface selectDistinct(RawQuery|string $column, ?string $alias = null) + * @method static CRUDInterface selectCoalesce(RawQuery|string $column, mixed $default = '0', ?string $alias = null) + * @method static CRUDInterface selectSum(string|RawQuery $column, ?string $alias = null) + * @method static CRUDInterface selectConcat(array $columns, ?string $alias = null) + * @method static CRUDInterface from(RawQuery|string $table, ?string $alias = null) + * @method static CRUDInterface addFrom(RawQuery|string $table, ?string $alias = null) + * @method static CRUDInterface table(string|RawQuery $table) + * @method static CRUDInterface groupBy(string|RawQuery|array ...$columns) + * @method static CRUDInterface join(RawQuery|string $table, RawQuery|string|Closure $onStmt = null, string $type = 'INNER') + * @method static CRUDInterface selfJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) + * @method static CRUDInterface innerJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) + * @method static CRUDInterface leftJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) + * @method static CRUDInterface rightJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) + * @method static CRUDInterface leftOuterJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) + * @method static CRUDInterface rightOuterJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) + * @method static CRUDInterface naturalJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) + * @method static CRUDInterface orderBy(RawQuery|string $column, string $soft = 'ASC') + * @method static CRUDInterface where(RawQuery|string $column, string $operator = '=', mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface having(RawQuery|string $column, string $operator = '=', mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface on(RawQuery|string $column, string $operator = '=', mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface set(RawQuery|array|string $column, mixed $value = null, bool $strict = true) + * @method static CRUDInterface addSet(RawQuery|array|string $column, mixed $value = null, bool $strict = true) + * @method static CRUDInterface andWhere(string|RawQuery $column, string $operator = '=', mixed $value = null) + * @method static CRUDInterface orWhere(string|RawQuery $column, string $operator = '=', mixed $value = null) + * @method static CRUDInterface between(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null, string $logical = 'AND') + * @method static CRUDInterface orBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null) + * @method static CRUDInterface andBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null) + * @method static CRUDInterface notBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null, string $logical = 'AND') + * @method static CRUDInterface orNotBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null) + * @method static CRUDInterface andNotBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null) + * @method static CRUDInterface findInSet(string|RawQuery $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface andFindInSet(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface orFindInSet(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface notFindInSet(string|RawQuery $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface andNotFindInSet(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface orNotFindInSet(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface whereIn(string|RawQuery $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface whereNotIn(string|RawQuery $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface orWhereIn(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface orWhereNotIn(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface andWhereIn(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface andWhereNotIn(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface regexp(string|RawQuery $column, string|RawQuery $value, string $logical = 'AND') + * @method static CRUDInterface andRegexp(string|RawQuery $column, string|RawQuery $value) + * @method static CRUDInterface orRegexp(string|RawQuery $column, string|RawQuery $value) + * @method static CRUDInterface soundex(string|RawQuery $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface andSoundex(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface orSoundex(string|RawQuery $column, mixed $value = null) + * @method static CRUDInterface whereIsNull(string|RawQuery $column, string $logical = 'AND') + * @method static CRUDInterface orWhereIsNull(string|RawQuery $column) + * @method static CRUDInterface andWhereIsNull(string|RawQuery $column) + * @method static CRUDInterface whereIsNotNull(string|RawQuery $column, string $logical = 'AND') + * @method static CRUDInterface orWhereIsNotNull(string|RawQuery $column) + * @method static CRUDInterface andWhereIsNotNull(string|RawQuery $column) + * @method static CRUDInterface offset(int $offset = 0) + * @method static CRUDInterface limit(int $limit) + * @method static CRUDInterface like(string|RawQuery|array $column, mixed $value = null, string $type = 'both', string $logical = 'AND') + * @method static CRUDInterface orLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both') + * @method static CRUDInterface andLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both') + * @method static CRUDInterface notLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both', string $logical = 'AND') + * @method static CRUDInterface orNotLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both') + * @method static CRUDInterface andNotLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both') + * @method static CRUDInterface startLike(string|RawQuery|array $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface orStartLike(string|RawQuery|array $column, mixed $value = null) + * @method static CRUDInterface andStartLike(string|RawQuery|array $column, mixed $value = null) + * @method static CRUDInterface notStartLike(string|RawQuery|array $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface orStartNotLike(string|RawQuery|array $column, mixed $value = null) + * @method static CRUDInterface andStartNotLike(string|RawQuery|array $column, mixed $value = null) + * @method static CRUDInterface endLike(string|RawQuery|array $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface orEndLike(string|RawQuery|array $column, mixed $value = null) + * @method static CRUDInterface andEndLike(string|RawQuery|array $column, mixed $value = null) + * @method static CRUDInterface notEndLike(string|RawQuery|array $column, mixed $value = null, string $logical = 'AND') + * @method static CRUDInterface orEndNotLike(string|RawQuery|array $column, mixed $value = null) + * @method static CRUDInterface andEndNotLike(string|RawQuery|array $column, mixed $value = null) + * @method static RawQuery subQuery(Closure $closure, ?string $alias = null, bool $isIntervalQuery = true) + * @method static CRUDInterface group(Closure $closure) + * @method static RawQuery raw(mixed $rawQuery) + * @method static string[] getErrors() */ class DB { - private static Database $databaseInstance; + private static CRUDInterface $databaseInstance; public function __call($name, $arguments) { @@ -156,19 +167,21 @@ public static function __callStatic($name, $arguments) return self::getDatabase()->{$name}(...$arguments); } - public static function createImmutable(array $credentials): Database + public static function createImmutable(array|ConnectionInterface $credentialsOrConnection): CRUDInterface { - return self::$databaseInstance = new Database($credentials); + if (isset(self::$databaseInstance)) { + return self::$databaseInstance; + } + + return self::$databaseInstance = new CRUD(new Database(($credentialsOrConnection instanceof ConnectionInterface) ? $credentialsOrConnection : new Connection($credentialsOrConnection), new QueryBuilder())); } - public static function connect(array $credentials): Database + public static function connect(array|ConnectionInterface $credentialsOrConnection): CRUDInterface { - return isset(self::$databaseInstance) - ? self::getDatabase()->newInstance($credentials) - : self::createImmutable($credentials); + return new CRUD(new Database((($credentialsOrConnection instanceof ConnectionInterface) ? $credentialsOrConnection : new Connection($credentialsOrConnection)), new QueryBuilder())); } - private static function getDatabase(): Database + public static function getDatabase(): CRUDInterface { return self::$databaseInstance; } diff --git a/src/Helpers/Helper.php b/src/Helpers/Helper.php deleted file mode 100644 index 763b866..0000000 --- a/src/Helpers/Helper.php +++ /dev/null @@ -1,123 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.1 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Helpers; - -use InitPHP\Database\Exceptions\ModelRelationsException; -use InitPHP\Database\Model; -use InitPHP\Database\Raw; - -final class Helper -{ - - private static array $modelInstance = []; - - public static function str_starts_with(string $haystack, string $needle): bool - { - if(\function_exists('str_starts_with')){ - return \str_starts_with($haystack, $needle); - } - return 0 === \strncmp($haystack, $needle, \strlen($needle)); - } - - public static function str_contains(string $haystack, string $needle): bool - { - if(\function_exists('str_contains')){ - return \str_contains($haystack, $needle); - } - return $needle === '' || FALSE !== \strpos($haystack, $needle); - } - - public static function str_ends_with(string $haystack, string $needle): bool - { - if(\function_exists('str_ends_with')){ - return \str_ends_with($haystack, $needle); - } - if($needle === '' || $needle === $haystack){ - return true; - } - if($haystack === ''){ - return false; - } - $need_len = \strlen($needle); - - return $need_len <= \strlen($haystack) && 0 === \substr_compare($haystack, $needle, (0 - $need_len)); - } - - public static function isSQLParameterOrFunction($value): bool - { - return ((\is_string($value)) && ( - $value === '?' - || (bool)\preg_match('/^:[\w]+$/', $value) - || (bool)\preg_match('/^[a-zA-Z_]+[\.]+[a-zA-Z_]+$/', $value) - || (bool)\preg_match('/^[a-zA-Z_]+\(\)$/', $value) - )) || ($value instanceof Raw) || \is_int($value); - } - - public static function isSQLParameter($value): bool - { - return (\is_string($value)) && ($value === '?' || (bool)\preg_match('/^:[\w]+$/', $value)); - } - - public static function camelCaseToSnakeCase($camelCase): string - { - $camelCase = \lcfirst($camelCase); - $split = \preg_split('', $camelCase, -1, \PREG_SPLIT_NO_EMPTY); - $snake_case = ''; - $i = 0; - foreach ($split as $row) { - $snake_case .= ($i === 0 ? '_' : '') - . \strtolower($row); - ++$i; - } - return \lcfirst($snake_case); - } - - public static function snakeCaseToCamelCase($snake_case): string - { - return \lcfirst(self::snakeCaseToPascalCase($snake_case)); - } - - public static function snakeCaseToPascalCase($snake_case): string - { - $split = \explode('_', \strtolower($snake_case)); - $camelCase = ''; - foreach ($split as $row) { - $camelCase .= \ucfirst($row); - } - return $camelCase; - } - - public static function getModelInstance($model): Model - { - if ($model instanceof Model) { - $class = \get_class($model); - return self::$modelInstance[$class] = $model; - } elseif (\is_string($model) && \class_exists($model)) { - if (isset(self::$modelInstance[$model])) { - return self::$modelInstance[$model]; - } - } else { - throw new \InvalidArgumentException(); - } - - $reflection = new \ReflectionClass($model); - if ($reflection->isSubclassOf(Model::class) === FALSE) { - throw new ModelRelationsException('The target class must be a subclass of \\InitPHP\\Database\\Model.'); - } - $class = $reflection->getName(); - - return self::$modelInstance[$class] = $reflection->newInstance(); - } - -} diff --git a/src/Helpers/Parameters.php b/src/Helpers/Parameters.php deleted file mode 100644 index 413cc1d..0000000 --- a/src/Helpers/Parameters.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.8 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Helpers; - -use InitPHP\Database\Raw; - -final class Parameters -{ - - private static array $parameters = []; - - public static function get(bool $reset = true): array - { - $parameters = self::$parameters; - if($reset === TRUE){ - self::reset(); - } - return $parameters; - } - - public static function set(string $key, $value): void - { - $key = ':' . \ltrim(\str_replace('.', '', $key), ':'); - self::$parameters[$key] = $value; - } - - public static function add($key, $value): string - { - if ($value === null) { - return 'NULL'; - } - if ($key instanceof Raw) { - $key = \md5((string)$key); - } - $originKey = \ltrim(\str_replace('.', '', $key), ':'); - $i = 0; - do{ - $key = ':' . ($i === 0 ? $originKey : $originKey . '_' . $i); - ++$i; - $hasParameter = isset(self::$parameters[$key]); - }while($hasParameter); - self::$parameters[$key] = $value; - return $key; - } - - public static function merge(...$array): void - { - self::$parameters = \array_merge(self::$parameters, ...$array); - } - - public static function getParam(string $key) - { - return self::$parameters[$key] ?? $key; - } - - public static function reset() - { - self::$parameters = []; - } - -} diff --git a/src/Helpers/Validation.php b/src/Helpers/Validation.php deleted file mode 100644 index 7a0f314..0000000 --- a/src/Helpers/Validation.php +++ /dev/null @@ -1,342 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Helpers; - -use InitPHP\Database\Database; -use InitPHP\Database\Exceptions\ValidationException; -use InitPHP\Database\Raw; - -final class Validation -{ - - protected const AVAILABLE_VALIDATION_METHODS = [ - 'required', 'string', 'int', 'float', 'numeric', - 'alpha', 'alphaNumeric', 'boolean', 'mail', 'url', - 'min', 'max', 'length', 'range', 'regex', - 'ip', 'ipv4', 'ipv6', 'equal', 'startWith', 'endWith', - 'strContains', 'only', - ]; - - protected const DEFAULT_MESSAGES = [ - 'required' => '{field} cannot be empty.', - 'string' => 'Must be {field} string.', - 'int' => 'Must be {field} integer.', - 'float' => 'Must be {field} float.', - 'unsigned' => 'Must be {field} unsigned.', - 'numeric' => 'Must be {field} numeric.', - 'alpha' => 'Must be {field} alphabetical.', - 'alphaNumeric' => 'Must be {field} alphanumeric.', - 'boolean' => 'Must be {field} boolean.', - 'mail' => 'Must be {field} mail.', - 'url' => 'Must be {field} url.', - 'min' => '{field} must be {1} or greater.', - 'max' => '{field} must be {1} or less.', - 'length' => '{field} can be minimum of {1} characters and a maximum of {2} characters.', - 'range' => '{field} can be at least {1} and at most {2}.', - 'regex' => '{field} is not in proper format/pattern.', - 'ip' => 'It should be {field} IP Address.', - 'ipv4' => '{field} should be an IPv4 Address.', - 'ipv6' => '{field} should be an IPv6 Address.', - 'equal' => '{field} can only be {1}', - 'startWith' => '{field} must start with {1}', - 'endWith' => '{field} must end with {1}', - 'strContains' => 'Must contain {field}, {1}', - 'only' => '{field} can only be; {arguments}', - //'is_unique' => '{field} must be unique.', - ]; - - private array $data = []; - - private array $methods = []; - private array $labels = []; - private array $messages = []; - private string $error; - - private Database $db; - - public function __construct(array $methods, array $messages, array $labels, Database &$db) - { - $this->methods = $methods; - $this->messages = $messages; - $this->labels = $labels; - $this->db = &$db; - } - - public function setData($data, array $parameters = []): self - { - $this->data = $data; - foreach ($parameters as $key => $value) { - $id = \array_search($key, $this->data); - if($id === FALSE){ - continue; - } - $this->data[$id] = $value; - } - return $this; - } - - public function getError(): ?string - { - return $this->error ?? null; - } - - public function validation(string $column, $schemaID = null): bool - { - unset($this->error); - if(!isset($this->methods[$column])){ - return true; - } - $methods = $this->methods[$column]; - if(\is_string($methods)){ - $methods = \explode('|', $methods); - } - - if(($key = \array_search('optional', $methods, true)) !== FALSE){ - if(!isset($this->data[$column])){ - return true; - } - unset($methods[$key]); - } - - $data = $this->data[$column] ?? null; - if ($data instanceof Raw) { - return true; - } - - if(Helper::isSQLParameter($data)){ - $data = Parameters::getParam($data); - } - if(($key = \array_search('is_unique', $methods, true)) !== FALSE){ - if($this->is_unique($data, $column, $schemaID) === FALSE){ - $this->error = $this->get_message($column, 'is_unique'); - return false; - } - unset($methods[$key]); - } - - foreach ($methods as $method) { - $method = $this->method_parse($method); - if(!\in_array($method['method'], self::AVAILABLE_VALIDATION_METHODS)){ - throw new ValidationException('The validation method "' . $method['method'] . '" defined for column "' . $column . '" is not available.'); - } - $arguments = $method['arguments']; - \array_unshift($arguments, $data); - $method = $method['method']; - $res = $this->{$method}(...$arguments); - if($res === FALSE){ - $this->error = $this->get_message($column, $method, $arguments); - return false; - } - } - return true; - } - - protected function is_unique($data, $column, $schemaID = null): bool - { - if($this->db->getSchemaID() === null){ - throw new ValidationException('You need a model with a PRIMARY KEY to use the is_unique validation.'); - } - $db = $this->db->clone(); - $db->reset(); - $parameters = Parameters::get(true); - $db->select($db->getSchemaID())->offset(0) - ->limit(1); - if(!empty($schemaID)){ - $db->where($db->getSchemaID(), $schemaID, '!='); - } - $db->where($column, $data, '='); - $res = $db->get($db->getSchema()); - unset($db); - Parameters::merge($parameters); - return $res->numRows() < 1; - } - - protected function required($data): bool - { - if(!\is_iterable($data) && !\is_object($data)){ - $data = \trim((string)$data); - return $data !== ''; - } - return !empty($data); - } - - protected function string($data): bool - { - return \is_string($data); - } - - protected function int($data): bool - { - return (bool)\preg_match('/^([+|\-]*)[0-9]+$/', (string)$data); - } - - protected function float($data): bool - { - return (bool)\preg_match('/^[+|\-]*[0-9]+[.]+[0-9]+$/', (string)$data); - } - - protected function unsigned($data): bool - { - return !((bool)\preg_match('/^[+|\-]+/', (string)$data)); - } - - protected function numeric($data):bool - { - return (bool)\preg_match('/^[+|\-]*[0-9]+([.]+[0-9]+)*$/', (string)$data); - } - - protected function alpha($data): bool - { - $pattern = '[a-zA-ZğĞŞşÜüİıÖöÇç]'; - return (bool)\preg_match('/^' . $pattern . '$/', (string)$data); - } - - protected function alphaNumeric($data): bool - { - $pattern = '(?:([+|\-]*[0-9]+([.]+[0-9]+)*)|([0-9a-zA-ZğĞŞşÜüİıÖöÇç]))+'; - return (bool)\preg_match('/^' . $pattern . '$/', (string)$data); - } - - protected function boolean($data): bool - { - return (bool)\preg_match('/^(true|false|0|1)]+$/', (string)$data); - } - - protected function mail($data): bool - { - return (bool)\filter_var($data, \FILTER_VALIDATE_EMAIL); - } - - protected function url($data): bool - { - return (bool)\filter_var($data, \FILTER_VALIDATE_URL); - } - - protected function min($data, $min = 0): bool - { - return $data >= $min; - } - - protected function max($data, $max = 1): bool - { - return $data <= $max; - } - - protected function length($data, $min = 0, $max = null): bool - { - $len = \strlen($data); - $res = $len >= $min; - if($res !== FALSE && $max !== null){ - return $len <= $max; - } - return $res; - } - - protected function range($data, $min = \PHP_INT_MIN, $max = \PHP_INT_MAX) - { - return $this->min($data, $min) && $this->max($data, $max); - } - - protected function regex($data, $pattern): bool - { - return (bool)\preg_match($pattern, (string)$data); - } - - protected function ip($data): bool - { - return (bool)\filter_var($data, \FILTER_VALIDATE_IP); - } - - protected function ipv4($data): bool - { - return (bool)\filter_var($data, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4); - } - - protected function ipv6($data): bool - { - return (bool)\filter_var($data, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6); - } - - protected function equal($data, $assert): bool - { - return $data == $assert; - } - - protected function startWith($data, $with): bool - { - return Helper::str_starts_with($data, $with); - } - - protected function endWith($data, $with): bool - { - return Helper::str_ends_with($data, $with); - } - - protected function strContains($data, $search): bool - { - return Helper::str_contains($data, $search); - } - - protected function only($data, ...$only): bool - { - foreach ($only as $row) { - if($row == $data){ - return true; - } - } - return false; - } - - private function method_parse(string $str): array - { - $arguments = []; - $method = $str; - if(\preg_match('/([\w]+)\((.+)\)/', $str, $params)){ - $method = $params[1]; - if(isset($params[2])){ - $split = \explode(',', $params[2]); - foreach ($split as $argument) { - $arguments[] = \trim(\trim($argument), '"\''); - } - } - } - - return [ - 'method' => $method, - 'arguments' => $arguments, - ]; - } - - private function get_message(string $column, string $method, $arguments = []): string - { - $msg = $this->messages[$column][$method] ?? self::DEFAULT_MESSAGES[$method]; - $label = $this->labels[$column] ?? $column; - - $replace = [ - '{field}' => $label - ]; - \array_shift($arguments); - if(!empty($arguments)){ - $replace['{arguments}'] = \implode(', ', $arguments); - $i = 1; - foreach ($arguments as $argument) { - $replace['{' . $i . '}'] = $argument; - ++$i; - } - } - return \strtr($msg, $replace); - } - - -} diff --git a/src/Init.php b/src/Init.php deleted file mode 100644 index bc3aa38..0000000 --- a/src/Init.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.8 - * @link https://www.muhammetsafak.com.tr - */ - - -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Helpers/Helper.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Helpers/Parameters.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Helpers/Validation.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/ConnectionException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/DeletableException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/ModelException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/ModelCallbacksException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/ModelRelationsException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/ReadableException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/SQLQueryExecuteException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/UpdatableException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/ValidationException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/WritableException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exceptions/ValueException.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'QueryBuilder.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Facade/DB.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Database.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Entity.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Model.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Raw.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Result.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Utils/Pagination.php'; -require_once __DIR__ . DIRECTORY_SEPARATOR . 'Utils/Datatables.php'; diff --git a/src/Model.php b/src/Model.php deleted file mode 100644 index f781bbb..0000000 --- a/src/Model.php +++ /dev/null @@ -1,627 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.1 - * @link https://www.muhammetsafak.com.tr - */ -declare(strict_types=1); - -namespace InitPHP\Database; - -use InitPHP\Database\Exceptions\{DeletableException, - ModelCallbacksException, - ModelException, - ModelRelationsException, - QueryBuilderException, - QueryGeneratorException, - ReadableException, - UpdatableException, - WritableException}; -use InitPHP\Database\Helpers\Helper; -use InitPHP\Database\Helpers\Parameters; - -abstract class Model extends Database -{ - - /** - * @var string[] - */ - protected array $connection; - - /** - * Dönüş için kullanılacak Entity sınıfı ya da nesnesi. - * - * @var Entity|string - */ - protected $entity = Entity::class; - - /** - * Modelin kullanacağı tablo adını tanımlar. Belirtilmez ya da boş bir değer belirtilirse model sınıfınızın adı kullanılır. - * - * @var string - */ - protected string $table; - - /** - * Tablonuzun PRIMARY KEY sütununun adını tanımlar. Eğer tablonuzda böyle bir sütun yoksa FALSE ya da NULL tanımlayın. - * - * @var null|string - */ - protected ?string $primaryKey = 'id'; - - /** - * Yumuşak silmenin kullanılıp kullanılmayacağını tanımlar. Eğer FALSE ise veri kalıcı olarak silinir. TRUE ise $deletedField doğru tanımlanmış bir sütun adı olmalıdır. - * - * @var bool - */ - protected bool $useSoftDeletes = true; - - /** - * Verinin eklenme zamanını ISO 8601 formatında tutacak sütun adı. - * - * @var string|null - */ - protected ?string $createdField = 'created_at'; - - /** - * Verinin son güncellenme zamanını ISO 8601 formatında tutacak sütun adı. Bu sütunun varsayılan değeri NULL olmalıdır. - * - * @var string|null - */ - protected ?string $updatedField = 'updated_at'; - - /** - * Yumuşak silme aktifse verinin silinme zamanını ISO 8601 formatında tutacak sütun adı. Bu sütun varsayılan değeri NULL olmalıdır. - * - * @var string|null - */ - protected ?string $deletedField = 'deleted_at'; - - /** - * Ekleme ve güncelleme gibi işlemlerde tanımlanmasına izin verilecek sütun isimlerini tutan dizi. - * - * @var string[] - */ - protected ?array $allowedFields = null; - - /** - * Ekleme, Silme ve Güncelleme işlemlerinde geri çağrılabilir yöntemlerin kullanılıp kullanılmayacağını tanımlar. - * - * @var bool - */ - protected bool $allowedCallbacks = false; - - /** - * Insert işlemi öncesinde çalıştırılacak yöntemleri tanımlar. Bu yöntemlere eklenmek istenen veri bir ilişkisel dizi olarak gönderilir ve geriye eklenecek veri dizisini döndürmesi gerekir. - * - * @var string[]|\Closure[] - */ - protected array $beforeInsert = []; - - /** - * Insert işlemi yürütüldükten sonra çalıştırılacak yöntemleri tanımlar. Eklenen veriyi ilişkisel bir dizi olarak alır ve yine bu diziyi döndürmesi gerekir. - * - * @var string[]|\Closure[] - */ - protected array $afterInsert = []; - - /** - * Update işlemi yürütülmeden önce çalıştırılacak yöntemleri tanımlar. Güncellenecek sütun ve değerleri ilişkisel bir dizi olarak gönderilir ve yine bu dizi döndürmesi gerekir. - * - * @var string[]|\Closure[] - */ - protected array $beforeUpdate = []; - - /** - * Update işlemi yürütüldükten sonra çalıştırılacak yöntemleri tanımlar. Güncellenmiş sütun ve değerleri ilişkisel bir dizi olarak gönderilir ve yine bu dizi döndürmesi gerekir. - * - * @var string[]|\Closure[] - */ - protected array $afterUpdate = []; - - /** - * Delete işlemi yürülmeden önce çalıştırılacak yöntemleri tanımlar.Etkilenecek satırların çoklu ilişkisel dizisi parametre olarak gönderilir ve yine bu dizi döndürmesi gerekir. - * - * @var string[]|\Closure[] - */ - protected array $beforeDelete = []; - - /** - * Delete işlemi yürütüldükten sonra çalıştırılacak yöntemleri tanımlar. Etkilenmiş satırların çoklu ilişkisel dizisi parametre olarak gönderilir ve yine bu dizi döndürmesi gerekir. - * - * @var string[]|\Closure[] - */ - protected array $afterDelete = []; - - /** - * Bu modelin veriyi okuyabilir mi olduğunu tanımlar. - * - * @var bool - */ - protected bool $readable = true; - - /** - * Bu modelin bir veri yazabilir mi olduğunu tanımlar. - * - * @var bool - */ - protected bool $writable = true; - - /** - * Bu modelin bir veri silebilir mi olduğunu tanımlar. - * - * @var bool - */ - protected bool $deletable = true; - - /** - * Bu modelin bir veriyi güncelleyebilir mi olduğunu tanımlar. - * - * @var bool - */ - protected bool $updatable = true; - - /** - * Hangi sütunların hangi doğrulama yöntemine uyması gerektiğini tanımlayan dizi. - * - * @var array - */ - protected array $validation = []; - - /** - * Sütun ve doğrulama yöntemlerine özel oluşacak hata mesajlarını özelleştirmenize/yerelleştirmeniz için kullanılan dizi. - * - * @var array - */ - protected array $validationMsg = []; - - /** - * Sütun ve doğrulama yöntemlerine özel oluşturulacak hata mesajlarında {field} yerini alacak sütun adı yerine kullanılacak değerleri tanımlayan ilişkisel dizi. - * - * @var array - */ - protected array $validationLabels = []; - - public function __construct() - { - $credentials = $this->connection ?? []; - if(!isset($this->table)){ - $modelClass = \get_called_class(); - $modelReflection = new \ReflectionClass($modelClass); - $this->table = \strtolower($modelReflection->getShortName()); - unset($modelClass, $modelReflection); - } - if($this->useSoftDeletes !== FALSE){ - if(empty($this->deletedField)){ - throw new ModelException('There must be a delete column to use soft delete.'); - } - } - $credentials['tableSchema'] = $this->table; - $credentials['tableSchemaID'] = $this->primaryKey ?? null; - $credentials['allowedFields'] = $this->allowedFields ?? null; - $credentials['createdField'] = $this->createdField ?? null; - $credentials['updatedField'] = $this->updatedField ?? null; - $credentials['deletedField'] = $this->deletedField ?? null; - if($credentials['allowedFields'] !== null){ - if($credentials['createdField'] !== null && !\in_array($credentials['createdField'], $credentials['allowedFields'])){ - $credentials['allowedFields'][] = $credentials['createdField']; - } - if($credentials['updatedField'] !== null && !\in_array($credentials['updatedField'], $credentials['allowedFields'])){ - $credentials['allowedFields'][] = $credentials['updatedField']; - } - if($credentials['deletedField'] !== null && !\in_array($credentials['deletedField'], $credentials['allowedFields'])){ - $credentials['allowedFields'][] = $credentials['deletedField']; - } - } - $credentials['entity'] = $this->entity ?? Entity::class; - $credentials['validation'] = [ - 'methods' => $this->validation ?? [], - 'messages' => $this->validationMsg ?? [], - 'labels' => $this->validationLabels ?? [], - ]; - $credentials['readable'] = $this->readable ?? true; - $credentials['updatable'] = $this->updatable ?? true; - $credentials['deletable'] = $this->deletable ?? true; - $credentials['writable'] = $this->writable ?? true; - $credentials['return'] = $this->return ?? self::ENTITY; - parent::__construct($credentials); - } - - final public function withPrimaryKey(string $column): self - { - $clone = clone $this; - $clone->primaryKey = $column; - $clone->setSchemaID($column); - return $clone; - } - - /** - * @param array $set - * @return array|false - */ - final public function create(?array $set = null) - { - return $this->insert($set); - } - - /** - * @param array $set - * @return array|false - */ - final public function createBatch(?array $set = null) - { - return $this->insertBatch($set); - } - - /** - * @param array $set - * @return array|bool - */ - final public function insert(?array $set = null) - { - if($this->isWritable() === FALSE){ - throw new WritableException('"' . \get_called_class() . '" is not a writable model.'); - } - - $data = $this->isCallbacksFunction('beforeInsert', 'afterInsert') ? $this->callbacksFunctionHandler($set, 'beforeInsert') : $set; - - if($data === FALSE){ - return false; - } - - if(parent::create($data) === FALSE){ - return false; - } - - return $this->isCallbacksFunction('afterInsert') ? $this->callbacksFunctionHandler($data, 'afterInsert') : true; - } - - /** - * @param array $set - * @return array|false - */ - final public function insertBatch(array $set) - { - if($this->isUpdatable() === FALSE){ - throw new UpdatableException('"' . \get_called_class() . '" is not a updatable model.'); - } - - if($this->isCallbacksFunction('beforeInsert', 'afterInsert')){ - foreach ($set as &$data) { - $data = $this->callbacksFunctionHandler($data, 'beforeInsert'); - if($data === FALSE){ - return false; - } - } - } - - if(parent::createBatch($set) === FALSE){ - return false; - } - - if($this->isCallbacksFunction('afterInsert')){ - foreach ($set as &$row) { - $row = $this->callbacksFunctionHandler($row, 'afterInsert'); - if($row === FALSE){ - return false; - } - } - } - - return $set; - } - - /** - * @param Entity $entity - * @return array|bool - */ - final public function save(Entity $entity) - { - $data = $entity->toArray(); - $schemaID = $this->getSchemaID(); - if($schemaID !== null && isset($data[$schemaID])){ - return $this->update($data); - } - return $this->insert($data); - } - - /** - * @param array $selector - * @param array $conditions - * @param array $parameters - * @return Result - */ - final public function read(array $selector = [], array $conditions = [], array $parameters = []) - { - if($this->isReadable() === FALSE){ - throw new ReadableException('"' . \get_called_class() . '" is not a readable model.'); - } - return parent::read($selector, $conditions, $parameters); - } - - /** - * @param array $selector - * @param array $conditions - * @param array $parameters - * @return Result - */ - final public function readOne(array $selector = [], array $conditions = [], array $parameters = []) - { - if($this->isReadable() === FALSE){ - throw new ReadableException('"' . \get_called_class() . '" is not a readable model.'); - } - return parent::readOne($selector, $conditions, $parameters); - } - - /** - * @param array $set - * @return array|bool - */ - final public function update(?array $set = null) - { - if($this->isUpdatable() === FALSE){ - throw new UpdatableException('"' . \get_called_class() . '" is not a updatable model.'); - } - $data = $this->isCallbacksFunction('beforeUpdate', 'afterUpdate') ? $this->callbacksFunctionHandler($set, 'beforeUpdate') : $set; - if($data === FALSE){ - return false; - } - if(parent::update($data) === FALSE){ - return false; - } - - return $this->isCallbacksFunction('afterUpdate') ? $this->callbacksFunctionHandler($data, 'afterUpdate') : true; - } - - /** - * @param string $referenceColumn - * @param array|null $set - * @return array|false - * @throws QueryBuilderException - * @throws QueryGeneratorException - */ - final public function updateBatch(string $referenceColumn, ?array $set = null) - { - if($this->isUpdatable() === FALSE){ - throw new UpdatableException('"' . \get_called_class() . '" is not a updatable model.'); - } - - if($this->isCallbacksFunction('beforeUpdate', 'afterUpdate')){ - foreach ($set as &$data) { - $data = $this->callbacksFunctionHandler($data, 'beforeUpdate'); - if($data === FALSE){ - return false; - } - } - } - - if(parent::updateBatch($referenceColumn, $set) === FALSE){ - return false; - } - - if($this->isCallbacksFunction('afterUpdate')){ - foreach ($set as &$row) { - $row = $this->callbacksFunctionHandler($row, 'afterUpdate'); - if($row === FALSE){ - return false; - } - } - } - - return $set; - } - - /** - * @param int|string|null $id - * @return array|bool - */ - final public function delete($id = null) - { - if($this->isDeletable() === FALSE){ - throw new DeletableException('"' . \get_called_class() . '" is not a deletable model.'); - } - if($id !== null && !empty($this->getSchemaID())){ - $this->where($this->getSchemaID(), $id); - } - - if ($this->isCallbacksFunction('beforeDelete', 'afterDelete')) { - $clone = clone $this; - $parameters = Parameters::get(); - $res = $clone->query($clone->_readQuery(), $parameters); - $data = $res->asAssoc()->results(); - Parameters::merge($parameters); - unset($clone, $parameters); - - if($data === null){ - return true; - } - $data = $this->callbacksFunctionHandler($data, 'beforeDelete'); - if($data === FALSE){ - return false; - } - } - - if(parent::delete() === FALSE){ - return false; - } - - return $this->isCallbacksFunction('afterDelete') ? $this->callbacksFunctionHandler($data, 'afterDelete') : true; - } - - final public function purgeDeleted(): bool - { - if($this->isDeletable() === FALSE){ - return false; - } - $this->onlyDeleted(); - return parent::delete(); - } - - /** - * İki model arasında bir JOIN ilişkisi kurar. - * - * @param string|Model $model - * @param string|null $foreignKey - * @param string|null $localKey - * @param string $joinType - * @return $this - */ - final public function relation($model, ?string $foreignKey = null, ?string $localKey = null, string $joinType = 'INNER'): self - { - $from = [ - 'tableSchema' => $this->getSchema(), - 'tableSchemaID' => $this->getSchemaID(), - ]; - $target = $this->getModelTableNameAndPrimaryKeyColumn($model); - - if($localKey === null || $localKey === '{primaryKey}'){ - if(empty($from['tableSchemaID'])){ - throw new ModelRelationsException('To use relationships, the model must have a primary key column.'); - } - }else{ - $from['tableSchemaID'] = $localKey; - } - if($foreignKey === null) { - $target['tableSchemaID'] = (Helper::str_ends_with($from['tableSchema'], 's') ? \substr($from['tableSchema'], -1) : $from['tableSchema']) - . '_' . $from['tableSchemaID']; - } elseif ($foreignKey === '{primaryKey}') { - if(empty($target['tableSchemaID'])){ - throw new ModelRelationsException('To use relationships, the model must have a primary key column.'); - } - }else{ - $target['tableSchemaID'] = $foreignKey; - } - $this->join($target['tableSchema'], ($from['tableSchema'] . '.' . $from['tableSchemaID'] . ' = ' . $target['tableSchema'] . '.' . $target['tableSchemaID']), $joinType); - - return $this; - } - - /** - * @return bool - */ - final public function isWritable(): bool - { - return $this->writable ?? true; - } - - /** - * @return bool - */ - final public function isReadable(): bool - { - return $this->readable ?? true; - } - - /** - * @return bool - */ - final public function isUpdatable(): bool - { - return $this->updatable ?? true; - } - - /** - * @return bool - */ - final public function isDeletable(): bool - { - return $this->deletable ?? true; - } - - /** - * @param string ...$methods - * @return bool - */ - private function isCallbacksFunction(string ...$methods): bool - { - if(($this->allowedCallbacks ?? false) === FALSE){ - return false; - } - foreach ($methods as $method) { - $callbacks = $this->{$method} ?? null; - if(!empty($callbacks)){ - return true; - } - } - return false; - } - - /** - * @param array $data - * @param string $method - * @return array|false - */ - private function callbacksFunctionHandler(array $data, string $method) - { - if(!$this->isCallbacksFunction($method)){ - return $data; - } - $callbacks = $this->{$method}; - - foreach ($callbacks as $callback) { - if(\is_string($callback)){ - if(\method_exists($this, $callback) === FALSE){ - continue; - } - $data = \call_user_func_array([$this, $callback], [$data]); - if($data === FALSE){ - return false; - } - if(!\is_array($data)){ - throw new ModelCallbacksException('Callbacks methods must return an array or false.'); - } - continue; - } - if(!\is_callable($callback)){ - throw new ModelCallbacksException('Callbacks methods can only be model methods or callable.'); - } - $data = \call_user_func_array($callback, [$data]); - if($data === FALSE){ - return false; - } - if(!\is_array($data)){ - throw new ModelCallbacksException('Callbacks methods must return an array or false.'); - } - } - - return $data; - } - - private function getModelTableNameAndPrimaryKeyColumn($model): array - { - $reflection = new \ReflectionClass($model); - if ($reflection->isSubclassOf(Model::class) === FALSE) { - throw new ModelRelationsException('The target class must be a subclass of \\InitPHP\\Database\\Model.'); - } - if (\defined('PHP_VERSION_ID') && \PHP_VERSION_ID >= 80000) { - $tableReflection = $reflection->getProperty('table'); - $primaryKeyReflection = $reflection->getProperty('primaryKey'); - if (null === $tableName = $tableReflection->getDefaultValue()) { - $tableName = \strtolower($reflection->getShortName()); - } - $primaryKey = $primaryKeyReflection->getDefaultValue(); - - return [ - 'tableSchema' => $tableName, - 'tableSchemaID' => $primaryKey, - ]; - } - - /** @var $model Model */ - if (!\is_object($model)) { - $model = $reflection->newInstance(); - } - - return [ - 'tableSchema' => $model->getSchema(), - 'tableSchemaID' => $model->getSchemaID(), - ]; - } - -} diff --git a/src/Entity.php b/src/ORM/Entity.php similarity index 59% rename from src/Entity.php rename to src/ORM/Entity.php index fbbd912..65f6956 100644 --- a/src/Entity.php +++ b/src/ORM/Entity.php @@ -1,21 +1,12 @@ - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database; - -use InitPHP\Database\Helpers\Helper; - -class Entity + +namespace InitPHP\Database\ORM; + +use InitPHP\Database\ORM\Exceptions\EntityNotMethod; +use InitPHP\Database\ORM\Interfaces\EntityInterface; +use InitPHP\Database\Utils\Helper; + +class Entity implements EntityInterface { protected array $__attributes = []; @@ -27,27 +18,31 @@ public function __construct(?array $data = []) $this->setUp($data); } + /** + * @param $name + * @param $arguments + * @return mixed + * @throws EntityNotMethod + */ public function __call($name, $arguments) { - if(Helper::str_ends_with($name, 'Attribute') === FALSE){ - throw new \RuntimeException('There is no "' . $name . '" method.'); - } - switch (\substr($name, 0, 3)) { - case 'get': - $attr = Helper::camelCaseToSnakeCase(\substr($name, 3, -9)); - return $this->__attributes[$attr] ?? null; - case 'set': - $attr = Helper::camelCaseToSnakeCase(\substr($name, 3, -9)); - return $this->__attributes[$attr] = $arguments[0]; - default: - throw new \RuntimeException('There is no "' . $name . '" method.'); + if (str_ends_with($name, 'Attribute') === false) { + throw new EntityNotMethod($name); } + + $attr = Helper::camelCaseToSnakeCase(substr($name, 3, -9)); + + return match (substr($name, 0, 3)) { + 'get' => $this->__attributes[$attr] ?? null, + 'set' => $this->__attributes[$attr] = $arguments[0], + default => throw new EntityNotMethod($name) + }; } public function __set($name, $value) { $methodName = 'set' . Helper::snakeCaseToPascalCase($name) . 'Attribute'; - if(\method_exists($this, $methodName)){ + if(method_exists($this, $methodName)){ $this->{$methodName}($value); return $value; } @@ -57,7 +52,7 @@ public function __set($name, $value) public function __get($name) { $methodName = 'get' . Helper::snakeCaseToPascalCase($name) . 'Attribute'; - if(\method_exists($this, $methodName)){ + if(method_exists($this, $methodName)){ return $this->{$methodName}(); } return $this->__attributes[$name] ?? null; @@ -80,16 +75,26 @@ public function __debugInfo() return $this->__attributes; } + /** + * @inheritDoc + */ public function toArray(): array { return $this->__attributes; } + /** + * @inheritDoc + */ public function getAttributes(): array { return $this->toArray(); } + /** + * @param array|null $data + * @return $this + */ protected function setUp(?array $data = null): self { $this->syncOriginal() @@ -97,6 +102,10 @@ protected function setUp(?array $data = null): self return $this; } + /** + * @param array|null $data + * @return $this + */ protected function fill(?array $data = null): self { if($data !== null){ @@ -107,6 +116,9 @@ protected function fill(?array $data = null): self return $this; } + /** + * @return $this + */ protected function syncOriginal(): self { $this->__attributesOriginal = $this->__attributes; diff --git a/src/ORM/Exceptions/DeletableException.php b/src/ORM/Exceptions/DeletableException.php new file mode 100644 index 0000000..67c7696 --- /dev/null +++ b/src/ORM/Exceptions/DeletableException.php @@ -0,0 +1,7 @@ +schema)) { + $modelClass = get_called_class(); + $modelReflection = new ReflectionClass($modelClass); + $this->schema = Helper::camelCaseToSnakeCase($modelReflection->getShortName()); + unset($modelClass, $modelReflection); + } + if ($this->useSoftDeletes !== false && empty($this->deletedField)) { + throw new ModelException('There must be a delete column to use soft delete.'); + } + + $this->crud = empty($this->credentials) ? DB::getDatabase() : DB::connect($this->credentials); + } + + public function __call(string $name, array $arguments) + { + $res = $this->crud->{$name}(...$arguments); + + return ($res instanceof CRUDInterface) ? $this : $res; + } + + /** + * @inheritDoc + */ + public function create(array $set = []): bool + { + if (!$this->writable) { + throw new WritableException(); + } + + !empty($this->createdField) && $set[$this->createdField] = date($this->timestampFormat); + + return $this->crud->create($this->schema, $set); + } + + /** + * @inheritDoc + */ + public function createBatch(array $set = []): bool + { + if (!$this->writable) { + throw new WritableException(); + } + $createdField = $this->createdField; + if (!empty($createdField) && !empty($set)) { + foreach ($set as &$row) { + $row[$createdField] = date($this->timestampFormat); + } + } + + return $this->crud->createBatch($this->schema, $set); + } + + /** + * @inheritDoc + */ + public function read(array $selector = [], array $conditions = [], array $parameters = []): ResultInterface + { + if (!$this->readable) { + throw new ReadableException(); + } + if ($this->useSoftDeletes) { + if ($this->isOnlyDelete) { + $this->onlyDeleted(); + } else { + $this->ignoreDeleted(); + } + $this->isOnlyDelete = false; + } + + return $this->crud + ->read($this->schema, $selector, $conditions, $parameters) + ->asClass($this->entity); + } + + /** + * @inheritDoc + */ + public function readOne(array $selector = [], array $conditions = [], array $parameters = []): ResultInterface + { + if (!$this->readable) { + throw new ReadableException(); + } + if ($this->useSoftDeletes) { + if ($this->isOnlyDelete) { + $this->onlyDeleted(); + } else { + $this->ignoreDeleted(); + } + $this->isOnlyDelete = false; + } + + return $this->crud + ->readOne($this->schema, $selector, $conditions, $parameters) + ->asClass($this->entity); + } + + /** + * @inheritDoc + */ + public function update(array $set = []): bool + { + if (!$this->updatable) { + throw new UpdatableException(); + } + + if (!empty($this->schemaId) && isset($set[$this->schemaId])) { + $this->where($this->schemaId, $set[$this->schemaId]); + unset($set[$this->schemaId]); + } + + !empty($this->updatedField) && $set[$this->updatedField] = date($this->timestampFormat); + + $this->ignoreDeleted(); + + return $this->crud->update($this->schema, $set); + } + + /** + * @inheritDoc + */ + public function updateBatch(array $set = [], ?string $referenceColumn = null): bool + { + if (!$this->updatable) { + throw new UpdatableException(); + } + $updatedField = $this->updatedField; + if (!empty($updatedField) && !empty($set)) { + foreach ($set as &$row) { + $row[$updatedField] = date($this->timestampFormat); + } + } + $this->ignoreDeleted(); + + return $this->crud->updateBatch($this->schema, $set, $referenceColumn ?? $this->schemaId); + } + + /** + * @inheritDoc + */ + public function delete(?array $conditions = null, bool $purge = false): bool + { + if (!$this->deletable) { + throw new DeletableException(); + } + if ($this->useSoftDeletes && $purge === false) { + $this->ignoreDeleted() + ->set($this->deletedField, date($this->timestampFormat)); + + if (!empty($conditions)) { + foreach ($conditions as $column => $value) { + if (is_string($column)) { + $this->crud->getQueryBuilder()->where($column, $value); + } else { + $this->crud->getQueryBuilder()->where($value); + } + } + } + + return $this->crud->update($this->schema); + } + + return $this->crud->delete($this->schema, $conditions); + } + + /** + * @inheritDoc + */ + public function save(EntityInterface $entity): bool + { + $data = $entity->toArray(); + + return !empty($this->schemaId) && isset($data[$this->schemaId]) ? $this->update($data) : $this->create($data); + } + + /** + * @inheritDoc + */ + public function ignoreDeleted(): self + { + $this->useSoftDeletes && $this->crud->getQueryBuilder() + ->whereIsNull($this->deletedField); + + return $this; + } + + /** + * @inheritDoc + */ + public function onlyDeleted(): self + { + $this->useSoftDeletes && $this->crud->getQueryBuilder() + ->whereIsNotNull($this->deletedField); + + return $this; + } + +} diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php deleted file mode 100644 index 780509c..0000000 --- a/src/QueryBuilder.php +++ /dev/null @@ -1,1618 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.1 - * @link https://www.muhammetsafak.com.tr - */ -declare(strict_types=1); - -namespace InitPHP\Database; - -use \InitPHP\Database\Helpers\{Helper, Parameters}; -use \InitPHP\Database\Exceptions\{QueryBuilderException, QueryGeneratorException, ValueException}; - -class QueryBuilder -{ - - protected const STRUCTURE = [ - 'select' => [], - 'table' => [], - 'join' => [], - 'where' => [ - 'AND' => [], - 'OR' => [], - ], - 'having' => [ - 'AND' => [], - 'OR' => [], - ], - 'group_by' => [], - 'order_by' => [], - 'offset' => null, - 'limit' => null, - 'set' => [], - 'on' => [ - 'AND' => [], - 'OR' => [], - ], - ]; - - protected array $_STRUCTURE = self::STRUCTURE; - - protected bool $isOnlyDeletes = false; - - public function reset(): void - { - $this->_STRUCTURE = self::STRUCTURE; - } - - public function importQB(array $structure): self - { - $this->_STRUCTURE = $structure; - - return $this; - } - - public function exportQB(): array - { - return $this->_STRUCTURE; - } - - /** - * @param string $key - * @param string|int|float|bool|null $value - * @return $this - */ - final public function setParameter(string $key, $value): self - { - Parameters::set($key, $value); - return $this; - } - - /** - * @param array $parameters - * @return $this - */ - final public function setParameters(array $parameters = []): self - { - foreach ($parameters as $key => $value) { - Parameters::set($key, $value); - } - return $this; - } - - /** - * @param string|Raw ...$columns - * @return $this - */ - final public function select(...$columns): self - { - foreach ($columns as $column) { - $this->_STRUCTURE['select'][] = (string)$column; - } - - return $this; - } - - /** - * @param string|Raw $column - * @param string|null $alias - * @return $this - */ - final public function selectCount($column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'COUNT(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param string|null $alias - * @return $this - */ - final public function selectMax($column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'MAX(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param string|null $alias - * @return $this - */ - final public function selectMin($column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'MIN(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param string|null $alias - * @return $this - */ - final public function selectAvg($column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'AVG(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param string $alias - * @return $this - */ - final public function selectAs($column, string $alias): self - { - $this->_STRUCTURE['select'][] = $column . ' AS ' . $alias; - return $this; - } - - /** - * @param string|Raw $column - * @param string|null $alias - * @return $this - */ - final public function selectUpper($column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'UPPER(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param string|null $alias - * @return $this - */ - final public function selectLower($column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'LOWER(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param string|null $alias - * @return $this - */ - final public function selectLength($column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'LENGTH(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param int $offset - * @param int $length - * @param string|null $alias - * @return $this - */ - final public function selectMid($column, int $offset, int $length, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'MID(' . $column . ', ' . $offset . ', ' . $length . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param int $length - * @param string|null $alias - * @return $this - */ - final public function selectLeft($column, int $length, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'LEFT(' . $column . ', ' . $length . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param int $length - * @param string|null $alias - * @return $this - */ - final public function selectRight($column, int $length, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'RIGHT(' . $column . ', ' . $length . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param string|null $alias - * @return $this - */ - final public function selectDistinct($column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'DISTINCT(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw $column - * @param mixed $default - * @param string|null $alias - * @return $this - */ - final public function selectCoalesce($column, $default = '0', ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'COALESCE(' . $column . ', ' . $default . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - final public function selectSum(string $column, ?string $alias = null): self - { - $this->_STRUCTURE['select'][] = 'SUM(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|null $alias - * @param string|Raw ...$columnOrStr - * @return $this - */ - final public function selectConcat(?string $alias = null, ...$columnOrStr): self - { - foreach ($columnOrStr as &$item) { - $item = (string)$item; - } - - $this->_STRUCTURE['select'][] = 'CONCAT(' . \implode(', ', $columnOrStr) . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @param string|Raw ...$tables - * @return $this - */ - final public function from(...$tables): self - { - $this->_STRUCTURE['table'] = []; - $this->addFrom(...$tables); - - return $this; - } - - /** - * @param string|Raw ...$tables - * @return $this - */ - final public function addFrom(...$tables): self - { - foreach ($tables as $table) { - $table = (string)$table; - if(!\in_array($table, $this->_STRUCTURE['table'], true)){ - $this->_STRUCTURE['table'][] = $table; - } - } - return $this; - } - - /** - * @param string|Raw $table - * @return $this - */ - final public function table($table): self - { - $this->_STRUCTURE['table'] = [(string)$table]; - - return $this; - } - - /** - * @param string|Raw ...$columns - * @return $this - */ - final public function groupBy(...$columns): self - { - foreach ($columns as $column){ - $column = (string)$column; - if(!\in_array($column, $this->_STRUCTURE['group_by'])){ - $this->_STRUCTURE['group_by'][] = $column; - } - } - return $this; - } - - /** - * @param string|Raw $table - * @param string|Raw|\Closure|null $onStmt - * @param string $type - * @return $this - */ - final public function join($table, $onStmt = null, string $type = 'INNER'): self - { - $table = (string)$table; - - if ($onStmt instanceof \Closure) { - $queryBuilder = new self(); - $queryBuilder->_STRUCTURE = self::STRUCTURE; - $onStmt = \call_user_func_array($onStmt, [$queryBuilder]); - if ($onStmt === null) { - if ($where = $queryBuilder->__generateWhereQuery()) { - $this->where($this->raw($where)); - } - if ($having = $queryBuilder->__generateHavingQuery()) { - if (Helper::str_starts_with($having, ' HAVING ')) { - $having = \substr($having, 8); - } - $this->having($this->raw($having)); - } - $onStmt = $queryBuilder->__generateOnQuery(); - } - - } - - $type = \trim(\strtoupper($type)); - switch ($type) { - case 'SELF' : - $this->addFrom($table); - $onStmt !== null && $this->where((\is_string($onStmt) ? $this->raw($onStmt) : $onStmt)); - break; - case 'NATURAL': - case 'NATURAL JOIN': - $this->_STRUCTURE['join'][$table] = 'NATURAL JOIN ' . $table; - break; - default: - $this->_STRUCTURE['join'][$table] = \trim(($type . ' JOIN ' . $table . ' ON ' . $onStmt)); - } - return $this; - } - - /** - * @param string|Raw $table - * @param string|Raw $onStmt - * @return $this - */ - final public function selfJoin($table, $onStmt): self - { - return $this->join($table, $onStmt, 'SELF'); - } - - /** - * @param string|Raw $table - * @param string|Raw $onStmt - * @return $this - */ - final public function innerJoin($table, $onStmt): self - { - return $this->join($table, $onStmt, 'INNER'); - } - - /** - * @param string|Raw $table - * @param string|Raw $onStmt - * @return $this - */ - final public function leftJoin($table, $onStmt): self - { - return $this->join($table, $onStmt, 'LEFT'); - } - - /** - * @param string|Raw $table - * @param string|Raw $onStmt - * @return $this - */ - final public function rightJoin($table, $onStmt): self - { - return $this->join($table, $onStmt, 'RIGHT'); - } - - /** - * @param string|Raw $table - * @param string|Raw $onStmt - * @return $this - */ - final public function leftOuterJoin($table, $onStmt): self - { - return $this->join($table, $onStmt, 'LEFT OUTER'); - } - - /** - * @param string|Raw $table - * @param string|Raw $onStmt - * @return $this - */ - final public function rightOuterJoin($table, $onStmt): self - { - return $this->join($table, $onStmt, 'RIGHT OUTER'); - } - - /** - * @param string|Raw $table - * @return $this - */ - final public function naturalJoin($table): self - { - return $this->join($table, null, 'NATURAL'); - } - - /** - * @param string|Raw $column - * @param string $soft [ASC|DESC] - * @return $this - */ - final public function orderBy($column, string $soft = 'ASC'): self - { - $soft = \trim(\strtoupper($soft)); - if(!\in_array($soft, ['ASC', 'DESC'], true)){ - throw new \InvalidArgumentException('It can only sort as ASC or DESC.'); - } - $orderBy = \trim((string)$column) . ' ' . $soft; - if(!\in_array($orderBy, $this->_STRUCTURE['order_by'], true)){ - $this->_STRUCTURE['order_by'][] = $orderBy; - } - - return $this; - } - - /** - * @param Raw|string $column - * @param mixed $value - * @param string $mark [=|!=|>|<|>=|<=] - * @param string $logical [AND|OR] - * @return $this - */ - final public function where($column, $value = null, string $mark = '=', string $logical = 'AND'): self - { - $logical = \strtoupper(\strtr($logical, ['&&' => 'AND', '||' => 'OR'])); - if(!\in_array($logical, ['AND', 'OR'], true)){ - throw new \InvalidArgumentException('Logical operator OR, AND, && or || it could be.'); - } - - $this->_STRUCTURE['where'][$logical][] = $this->whereOrHavingStatementPrepare($column, $value, $mark); - - return $this; - } - - /** - * @param Raw|string $column - * @param mixed $value - * @param string $mark [=|!=|>|<|>=|<=] - * @param string $logical [AND|OR] - * @return $this - */ - final public function having($column, $value = null, string $mark = '=', string $logical = 'AND'): self - { - $logical = \strtoupper(\strtr($logical, ['&&' => 'AND', '||' => 'OR'])); - if(!\in_array($logical, ['AND', 'OR'], true)){ - throw new \InvalidArgumentException('Logical operator OR, AND, && or || it could be.'); - } - - $this->_STRUCTURE['having'][$logical][] = $this->whereOrHavingStatementPrepare($column, $value, $mark); - - return $this; - } - - final public function on($column, $value = null, string $mark = '=', string $logical = 'AND'): self - { - $logical = \strtoupper(\strtr($logical, ['&&' => 'AND', '||' => 'OR'])); - if (!\in_array($logical, ['AND', 'OR'])) { - throw new \InvalidArgumentException('Logical operator OR, AND, && or || it could be.'); - } - - $this->_STRUCTURE['on'][$logical][] = $this->whereOrHavingStatementPrepare($column, $value, $mark); - - return $this; - } - - /** - * @param string|Raw|array $column - * @param mixed $value - * @param bool $strict - * @return $this - * @throws QueryBuilderException - */ - final public function set($column, $value = null, bool $strict = true): self - { - return $this->addSet($column, $value, $strict); - } - - /** - * @param string|Raw|array $column - * @param mixed $value - * @param bool $strict - * @return $this - * @throws QueryBuilderException - */ - final public function addSet($column, $value = null, bool $strict = true): self - { - if (\is_array($column) && $value === null) { - $set = []; - foreach ($column as $name => $value) { - $this->checkSetData($name, $value, $strict); - $set[$name] = $value; - } - $this->_STRUCTURE['set'][] = $set; - } else { - $this->checkSetData($column, $value, $strict); - $this->_STRUCTURE['set'][][$column] = $value; - } - - return $this; - } - - /** - * @param string $column - * @return bool - */ - public function isAllowedFields(string $column): bool - { - return !($this instanceof Database) || $this->isAllowedFields($column); - } - - /** - * @param string|Raw $column - * @param mixed $value - * @param string $mark [=|!=|>|<|>=|<=] - * @return $this - */ - final public function andWhere($column, $value, string $mark = '='): self - { - return $this->where($column, $value, $mark, 'AND'); - } - - /** - * @param string|Raw $column - * @param mixed $value - * @param string $mark [=|!=|>|<|>=|<=] - * @return $this - */ - final public function orWhere($column, $value, string $mark = '='): self - { - return $this->where($column, $value, $mark, 'OR'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $values - * @param string $logical [AND|OR] - * @return $this - */ - final public function between($column, $values, string $logical = 'AND'): self - { - return $this->where($column, $values, 'BETWEEN', $logical); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $values - * @return $this - */ - final public function orBetween($column, $values): self - { - return $this->where($column, $values, 'BETWEEN', 'OR'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $values - * @return $this - */ - final public function andBetween($column, $values): self - { - return $this->where($column, $values, 'BETWEEN', 'AND'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $values - * @param string $logical [AND|OR] - * @return $this - */ - final public function notBetween($column, $values, string $logical = 'AND'): self - { - return $this->where($column, $values, 'NOTBETWEEN', $logical); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $values - * @return $this - */ - final public function orNotBetween($column, $values): self - { - return $this->where($column, $values, 'NOTBETWEEN', 'OR'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $values - * @return $this - */ - final public function andNotBetween($column, $values): self - { - return $this->where($column, $values, 'NOTBETWEEN', 'AND'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function findInSet($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'FINDINSET', $logical); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @return $this - */ - final public function orFindInSet($column, $value): self - { - return $this->where($column, $value, 'FINDINSET', 'OR'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @return $this - */ - final public function andFindInSet($column, $value): self - { - return $this->where($column, $value, 'FINDINSET', 'AND'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function notFindInSet(string $column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'NOTFINDINSET', $logical); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @return $this - */ - final public function andNotFindInSet($column, $value): self - { - return $this->where($column, $value, 'NOTFINDINSET', 'AND'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @return $this - */ - final public function orNotFindInSet($column, $value): self - { - return $this->where($column, $value, 'NOTFINDINSET', 'OR'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function in($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'IN', $logical); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @return $this - */ - final public function orIn($column, $value): self - { - return $this->where($column, $value, 'IN', 'OR'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @return $this - */ - final public function andIn($column, $value): self - { - return $this->where($column, $value, 'IN', 'AND'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function notIn($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'NOTIN', $logical); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @return $this - */ - final public function orNotIn($column, $value): self - { - return $this->where($column, $value, 'NOTIN', 'OR'); - } - - /** - * @param string|Raw $column - * @param array|Raw|string $value - * @return $this - */ - final public function andNotIn($column, $value): self - { - return $this->where($column, $value, 'NOTIN', 'AND'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function regexp($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'REGEXP', $logical); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function andRegexp($column, $value): self - { - return $this->where($column, $value, 'REGEXP', 'AND'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function orRegexp($column, $value): self - { - return $this->where($column, $value, 'REGEXP', 'OR'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function like($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'LIKE', $logical); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function orLike($column, $value): self - { - return $this->where($column, $value, 'LIKE', 'OR'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function andLike($column, $value): self - { - return $this->where($column, $value, 'LIKE', 'AND'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function startLike($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'STARTLIKE', $logical); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function orStartLike($column, $value): self - { - return $this->where($column, $value, 'STARTLIKE', 'OR'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function andStartLike($column, $value): self - { - return $this->where($column, $value, 'STARTLIKE', 'AND'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function endLike($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'ENDLIKE', $logical); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function orEndLike($column, $value): self - { - return $this->where($column, $value, 'ENDLIKE', 'OR'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function andEndLike($column, $value): self - { - return $this->where($column, $value, 'ENDLIKE', 'AND'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function notLike($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'NOTLIKE', $logical); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function orNotLike($column, $value): self - { - return $this->where($column, $value, 'NOTLIKE', 'OR'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function andNotLike($column, $value): self - { - return $this->where($column, $value, 'NOTLIKE', 'AND'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function startNotLike($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'STARTNOTLIKE', $logical); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function orStartNotLike($column, $value): self - { - return $this->where($column, $value, 'STARTNOTLIKE', 'OR'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function andStartNotLike($column, $value): self - { - return $this->where($column, $value, 'STARTNOTLIKE', 'AND'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function endNotLike($column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'ENDNOTLIKE', $logical); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function orEndNotLike($column, $value): self - { - return $this->where($column, $value, 'ENDNOTLIKE', 'OR'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function andEndNotLike($column, $value): self - { - return $this->where($column, $value, 'ENDNOTLIKE', 'AND'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function soundex(string $column, $value, string $logical = 'AND'): self - { - return $this->where($column, $value, 'SOUNDEX', $logical); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function orSoundex(string $column, $value): self - { - return $this->where($column, $value, 'SOUNDEX', 'OR'); - } - - /** - * @param string|Raw $column - * @param string|Raw $value - * @return $this - */ - final public function andSoundex($column, $value): self - { - return $this->where($column, $value, 'SOUNDEX', 'AND'); - } - - /** - * @param string|Raw $column - * @param null $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function is($column, $value = null, string $logical = 'AND'): self - { - return $this->where($column, $value, 'IS', $logical); - } - - /** - * @param string|Raw $column - * @param null $value - * @return $this - */ - final public function orIs($column, $value = null): self - { - return $this->where($column, $value, 'IS', 'OR'); - } - - /** - * @param string|Raw $column - * @param null $value - * @return $this - */ - final public function andIs($column, $value = null): self - { - return $this->where($column, $value, 'IS', 'AND'); - } - - /** - * @param string|Raw $column - * @param null $value - * @param string $logical [AND|OR] - * @return $this - */ - final public function isNot($column, $value = null, string $logical = 'AND'): self - { - return $this->where($column, $value, 'ISNOT', $logical); - } - - /** - * @param string|Raw $column - * @param null $value - * @return $this - */ - final public function orIsNot($column, $value = null): self - { - return $this->where($column, $value, 'ISNOT', 'OR'); - } - - /** - * @param string|Raw $column - * @param null $value - * @return $this - */ - final public function andIsNot($column, $value = null): self - { - return $this->where($column, $value, 'ISNOT', 'AND'); - } - - /** - * @param int $offset - * @return $this - */ - final public function offset(int $offset = 0): self - { - $this->_STRUCTURE['offset'] = (int)\abs($offset); - return $this; - } - - /** - * @param int $limit - * @return $this - */ - final public function limit(int $limit): self - { - $this->_STRUCTURE['limit'] = (int)\abs($limit); - - return $this; - } - - final public function exceptDeletedData(): self - { - if (!($this instanceof Database)) { - return $this; - } - if (empty($this->getCredentials('deletedField'))) { - return $this; - } - - return $this->isNot($this->getCredentials('deletedField'), null); - } - - final public function onlyDeletedData(): self - { - if (!($this instanceof Database)) { - return $this; - } - if (empty($this->getCredentials('deletedField'))) { - return $this; - } - - return $this->is($this->getCredentials('deletedField'), null); - } - - final public function raw(string $rawQuery): Raw - { - return new Raw($rawQuery); - } - - final public function subQuery(\Closure $closure, ?string $alias = null, bool $isIntervalQuery = true): Raw - { - $queryBuilder = new self(); - \call_user_func_array($closure, [$queryBuilder]); - - if ($alias !== null && $isIntervalQuery !== TRUE) { - throw new QueryBuilderException('To define alias to a subquery, it must be an inner query.'); - } - - $rawQuery = ($isIntervalQuery === TRUE ? '(' : '') - . $queryBuilder->generateSelectQuery() - . ($isIntervalQuery === TRUE ? ')' : '') - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this->raw($rawQuery); - } - - final public function generateInsertQuery(): string - { - if (!empty($this->_STRUCTURE['table'])) { - $table = \end($this->_STRUCTURE['table']); - } elseif ($this instanceof Database) { - $table = $this->getSchema(); - - $createdField = $this->getCredentials('createdField'); - if (!empty($createdField)) { - $this->addSet($createdField, \date($this->getCredentials('timestampFormat')), false); - } - } - - if (!isset($table)) { - throw new QueryGeneratorException('Table name not found when creating insert query.'); - } - - $columns = []; - $values = []; - $set = array_merge(...$this->_STRUCTURE['set']); - - foreach ($set as $column => $value) { - $columns[] = $column; - $values[] = $value; - } - if (empty($columns)) { - throw new QueryGeneratorException('The data set for the insert could not be found.'); - } - - return 'INSERT INTO ' . $table . ' (' . \implode(', ', $columns) . ') VALUES (' . \implode(', ', $values) . ');'; - } - - final public function generateBatchInsertQuery(): string - { - $singleStartValue = []; - if (!empty($this->_STRUCTURE['table'])) { - $table = \end($this->_STRUCTURE['table']); - } elseif ($this instanceof Database) { - $table = $this->getSchema(); - - $createdField = $this->getCredentials('createdField'); - if (!empty($createdField)) { - $singleStartValue = [ - $createdField => "'" . \date($this->getCredentials('timestampFormat')) . "'", - ]; - } - } - - if (!isset($table)) { - throw new QueryGeneratorException('Table name not found when creating insert query.'); - } - - $columns = \array_keys(\array_merge(...$this->_STRUCTURE['set'])); - - if (empty($columns)) { - throw new QueryGeneratorException('The data set for the insert could not be found.'); - } - - $values = []; - foreach ($this->_STRUCTURE['set'] as $set) { - $value = $singleStartValue; - foreach ($columns as $column) { - $value[$column] = $set[$column] ?? 'NULL'; - } - $values[] = '(' . \implode(', ', $value) . ')'; - } - - return 'INSERT INTO ' . $table . ' (' . \implode(', ', $columns) . ') VALUES ' . \implode(', ', $values) . ';'; - } - - final public function generateSelectQuery(array $selector = [], array $conditions = []): string - { - if(!empty($selector)){ - $this->select(...$selector); - } - if(!empty($conditions)){ - foreach ($conditions as $column => $value) { - if (\is_string($column)) { - $this->where($column, $value); - } else { - $this->where($value); - } - } - } - if ($this instanceof Database) { - $table = $this->getSchema(); - } - if (empty($this->_STRUCTURE['table']) && isset($table)) { - $this->_STRUCTURE['table'][] = $table; - } - - if (empty($this->_STRUCTURE['table'])) { - throw new QueryGeneratorException('Table name not found.'); - } - $this->__generateSoftDeleteQuery(); - - return 'SELECT ' - . (empty($this->_STRUCTURE['select']) ? '*' : \implode(', ', $this->_STRUCTURE['select'])) - . ' FROM ' - . \implode(', ', $this->_STRUCTURE['table']) - . (!empty($this->_STRUCTURE['join']) ? ' ' . \implode(' ', $this->_STRUCTURE['join']) : '') - . ' WHERE ' - . (($where = $this->__generateWhereQuery()) ? $where : '1') - . (!empty($this->_STRUCTURE['group_by']) ? ' GROUP BY ' . \implode(', ', $this->_STRUCTURE['group_by']) : '') - . ($this->__generateHavingQuery() ?? '') - . (!empty($this->_STRUCTURE['order_by']) ? ' ORDER BY ' . \implode(', ', $this->_STRUCTURE['order_by']) : '') - . ($this->__generateLimitQuery() ?? ''); - } - - final public function generateUpdateQuery(): string - { - $primaryKey = null; - if (!empty($this->_STRUCTURE['table'])) { - $table = \end($this->_STRUCTURE['table']); - } elseif ($this instanceof Database) { - $table = $this->getSchema(); - - $updatedField = $this->getCredentials('updatedField'); - if (!empty($updatedField)) { - $this->addSet($updatedField, \date($this->getCredentials('timestampFormat')), false); - } - $primaryKey = $this->getSchemaID(); - } - if (!isset($table)) { - throw new QueryGeneratorException('Table name not found.'); - } - - $set = array_merge(...$this->_STRUCTURE['set']); - - $updateSet = []; - foreach ($set as $column => $value) { - if ($primaryKey !== null && $column === $primaryKey) { - $this->where($column, $value); - continue; - } - $updateSet[] = $column . ' = ' . $value; - } - if (empty($updateSet)) { - throw new QueryGeneratorException('The data set for the insert could not be found.'); - } - $this->exceptDeletedData(); - - return 'UPDATE ' . $table . ' SET ' . \implode(', ', $updateSet) - . ' WHERE ' - . (($where = $this->__generateWhereQuery()) ? $where : '1') - . ($this->__generateHavingQuery() ?? '') - . ($this->__generateLimitQuery() ?? ''); - } - - final public function generateUpdateBatchQuery(string $referenceColumn) - { - $primaryKey = null; - $update = []; - if (!empty($this->_STRUCTURE['table'])) { - $table = \end($this->_STRUCTURE['table']); - } elseif ($this instanceof Database) { - $table = $this->getSchema(); - - $updatedField = $this->getCredentials('updatedField'); - if (!empty($updatedField)) { - $update[] = $updatedField . " = '" . \date($this->getCredentials('timestampFormat')) . "'"; - } - $primaryKey = $this->getSchemaID(); - } - if (!isset($table)) { - throw new QueryGeneratorException('Table name not found.'); - } - - $data = $this->_STRUCTURE['set']; - - $updateData = []; - $columns = []; - $where = []; - foreach ($data as $set) { - if (!isset($set[$referenceColumn])) { - throw new QueryGeneratorException('The reference column does not exist in one or more of the set arrays.'); - } - $setData = []; - foreach ($set as $key => $value) { - if ($primaryKey !== null && $key === $primaryKey) { - continue; - } - if ($key == $referenceColumn) { - $where[] = $value; - continue; - } - $setData[$key] = $value; - if (!\in_array($key, $columns)) { - $columns[] = $key; - } - } - $updateData[] = $setData; - } - - foreach ($columns as $column) { - $syntax = $column . ' = CASE'; - foreach ($updateData as $key => $values) { - if (!\array_key_exists($column, $values)) { - continue; - } - $syntax .= ' WHEN ' . $referenceColumn . ' = ' - . (Helper::isSQLParameterOrFunction($where[$key]) ? $where[$key] : Parameters::add($referenceColumn, $where[$key])) - . ' THEN ' - . $values[$column]; - } - $update[] = $syntax . ' ELSE ' . $column . ' END'; - } - - $this->in($referenceColumn, $where) - ->exceptDeletedData(); - - return 'UPDATE ' . $table . ' SET ' . \implode(', ', $update) - . ' WHERE ' - . (($where = $this->__generateWhereQuery()) ? $where : '1') - . ($this->__generateHavingQuery() ?? '') - . ($this->__generateLimitQuery() ?? ''); - } - - final public function generateDeleteQuery(): string - { - if (!empty($this->_STRUCTURE['table'])) { - $table = \end($this->_STRUCTURE['table']); - } elseif ($this instanceof Database) { - $table = $this->getSchema(); - } - if (!isset($table)) { - throw new QueryGeneratorException('Table name not found.'); - } - - return 'DELETE FROM' - . ' ' - . $table - . ' WHERE ' - . (($where = $this->__generateWhereQuery()) !== null ? $where : '1') - . ($this->__generateHavingQuery() ?? '') - . ($this->__generateLimitQuery() ?? ''); - } - - protected function __generateHavingQuery(): ?string - { - $isAndEmpty = empty($this->_STRUCTURE['having']['AND']); - $isOrEmpty = empty($this->_STRUCTURE['having']['OR']); - if($isAndEmpty && $isOrEmpty){ - return null; - } - return ' HAVING ' - . (!$isAndEmpty ? \implode(' AND ', $this->_STRUCTURE['having']['AND']) : '') - . (!$isAndEmpty && !$isOrEmpty ? ' AND ' : '') - . (!$isOrEmpty ? \implode(' OR ', $this->_STRUCTURE['having']['OR']) : ''); - } - - protected function __generateWhereQuery(): ?string - { - $isAndEmpty = empty($this->_STRUCTURE['where']['AND']); - $isOrEmpty = empty($this->_STRUCTURE['where']['OR']); - if($isAndEmpty && $isOrEmpty){ - return null; - } - return (!$isAndEmpty ? \implode(' AND ', $this->_STRUCTURE['where']['AND']) : '') - . (!$isAndEmpty && !$isOrEmpty ? ' AND ' : '') - . (!$isOrEmpty ? \implode(' OR ', $this->_STRUCTURE['where']['OR']) : ''); - } - - protected function __generateOnQuery(): ?string - { - $isAndEmpty = empty($this->_STRUCTURE['on']['AND']); - $isOrEmpty = empty($this->_STRUCTURE['on']['OR']); - if($isAndEmpty && $isOrEmpty){ - return null; - } - return (!$isAndEmpty ? \implode(' AND ', $this->_STRUCTURE['on']['AND']) : '') - . (!$isAndEmpty && !$isOrEmpty ? ' AND ' : '') - . (!$isOrEmpty ? \implode(' OR ', $this->_STRUCTURE['on']['OR']) : ''); - } - - protected function __generateLimitQuery(): ?string - { - if($this->_STRUCTURE['limit'] === null && $this->_STRUCTURE['offset'] === null){ - return null; - } - $sql = ' LIMIT '; - if($this->_STRUCTURE['offset'] !== null){ - $sql .= $this->_STRUCTURE['offset'] . ', '; - } - $sql .= $this->_STRUCTURE['limit'] ?? '10000'; - return $sql; - } - - protected function __generateSoftDeleteQuery(bool $reset = true): void - { - if ($this->isOnlyDeletes) { - $this->onlyDeletedData(); - } else { - $this->exceptDeletedData(); - } - if ($reset) { - $this->isOnlyDeletes = false; - } - } - - /** - * @param string|Raw $column - * @param mixed $value - * @param string $mark - * @return string - * @throws ValueException - */ - private function whereOrHavingStatementPrepare($column, $value, string $mark = '='): string - { - $mark = \trim($mark); - if ($value !== null && \in_array($mark, ['=', '!=', '<=', '>=', '>', '<'], true)) { - return $column . ' ' . $mark . ' ' - . (Helper::isSQLParameterOrFunction($value) ? $value : Parameters::add($column, $value)); - } - - $markUpperCase = \strtoupper($mark); - $searchMark = \str_replace([' ', '_'], '', $markUpperCase); - - if ($value === null && !\in_array($searchMark, ['IS', 'ISNOT'])) { - return (string)$column; - } - - switch ($searchMark) { - case 'IS': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = $value !== null ? Parameters::add($column, $value) : 'NULL'; - } - return $column . ' IS ' . $value; - case 'ISNOT': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = $value !== null ? Parameters::add($column, $value) : 'NULL'; - } - return $column . ' IS NOT ' . $value; - case 'LIKE': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = (substr($value, -1) == '%' || substr($value, 0, 1) == '%') - ? $value - : '%' . $value . '%'; - - $value = Parameters::add($column, $value); - } - return $column . ' LIKE ' . $value; - case 'STARTLIKE': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = Parameters::add($column, '%' . trim($value, '%')); - } - return $column . ' LIKE ' . $value; - case 'ENDLIKE': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = Parameters::add($column, trim($value, '%') . '%'); - } - return $column . ' LIKE ' . $value; - case 'NOTLIKE': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = (substr($value, -1) == '%' || substr($value, 0, 1) == '%') - ? $value - : '%' . $value . '%'; - - $value = Parameters::add($column, $value); - } - return $column . ' NOT LIKE ' . $value; - case 'STARTNOTLIKE': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = Parameters::add($column, '%' . trim($value, '%')); - } - return $column . ' NOT LIKE ' . $value; - case 'ENDNOTLIKE': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = Parameters::add($column, trim($value, '%') . '%'); - } - return $column . ' NOT LIKE ' . $value; - case 'REGEXP': - if(!Helper::isSQLParameterOrFunction($value)){ - $value = Parameters::add($column, $value); - } - return $column . ' REGEXP ' . $value; - case 'BETWEEN': - case 'NOTBETWEEN': - if (\is_array($value) && \count($value) == 2) { - $valueStmt = (Helper::isSQLParameterOrFunction($value[0]) ? $value[0] : Parameters::add($column, $value[0])) - . ' AND ' - . (Helper::isSQLParameterOrFunction($value[1]) ? $value[1] : Parameters::add($column, $value[1])); - } elseif (Helper::isSQLParameterOrFunction($value)) { - $valueStmt = (string)$value; - } else { - throw new ValueException('An incorrect value was defined.'); - } - return $column . ' ' - . ($searchMark === 'NOTBETWEEN' ? 'NOT ':'') - . 'BETWEEN ' . $valueStmt; - case 'IN': - case 'NOTIN': - if(\is_array($value)){ - $values = []; - foreach ($value as $val) { - if(\is_numeric($val) || \is_int($val)){ - if (!\in_array($val, $values)) { - $values[] = $val; - } - continue; - } - if($val === null){ - if (!\in_array('NULL', $values)) { - $values[] = 'NULL'; - } - continue; - } - $values[] = Helper::isSQLParameterOrFunction($val) ? $val : Parameters::add($column, $val); - } - $value = '(' . \implode(', ', \array_unique($values)) . ')'; - } elseif (Helper::isSQLParameterOrFunction($value)) { - $value = (string)$value; - }else{ - throw new ValueException('An incorrect value was defined.'); - } - return $column - . ($searchMark === 'NOTIN' ? ' NOT ' : ' ') - . 'IN ' . $value; - case 'FINDINSET': - case 'NOTFINDINSET': - if(\is_array($value)){ - $value = \implode(", ", $value); - } elseif (!Helper::isSQLParameterOrFunction($value)) { - $value = Parameters::add($column, $value); - } - return ($searchMark === 'NOTFINDINSET' ? 'NOT ' : '') - . 'FIND_IN_SET(' . $value . ', ' . $column . ')'; - case 'SOUNDEX': - if(!\is_string($value) && !($value instanceof Raw)){ - throw new ValueException('Only a string value can be defined for Soundex.'); - } - if(!Helper::isSQLParameterOrFunction($value)){ - $value = Parameters::add($column, $value); - } - return "SOUNDEX(" . $column . ") LIKE CONCAT('%', TRIM(TRAILING '0' FROM SOUNDEX(" . $value . ")), '%')"; - } - if($value === null && (bool)\preg_match('/([\w_]+)\((.+)\)$/iu', $column, $matches) !== FALSE){ - return \strtoupper($matches[1]) . '(' . $matches[2] . ')'; - } - return $column . ' ' . $mark . ' ' . Parameters::add($column, $value); - } - - private function checkSetData (&$column, &$value, $strict): void - { - $allowedColumnsCheck = ($strict === TRUE) && ($this instanceof Database) && !($column instanceof Raw); - if ($allowedColumnsCheck && !$this->isAllowedFields($column)) { - throw new QueryBuilderException('"' . $column . '" is not allowed.'); - } - $column = (string)$column; - $value = Helper::isSQLParameterOrFunction($value) ? $value : Parameters::add($column, $value); - } - -} diff --git a/src/QueryBuilder/Exceptions/QueryBuilderException.php b/src/QueryBuilder/Exceptions/QueryBuilderException.php new file mode 100644 index 0000000..2d5c661 --- /dev/null +++ b/src/QueryBuilder/Exceptions/QueryBuilderException.php @@ -0,0 +1,8 @@ +parameters = []; + } + + /** + * @inheritDoc + */ + public function set(string $key, mixed $value): self + { + $this->parameters[':' . ltrim(str_replace('.', '', $key), ':')] = $value; + + return $this; + } + + /** + * @inheritDoc + */ + public function add(RawQuery|string $key, mixed $value): string + { + if ($value === null) { + return 'NULL'; + } + if ($key instanceof RawQuery) { + $key = md5((string)$key); + } + $originKey = ltrim(str_replace('.', '', $key), ':'); + $i = 0; + do { + $key = ':' . ($i === 0 ? $originKey : $originKey . '_' . $i); + ++$i; + $hasParameter = isset($this->parameters[$key]); + } while($hasParameter); + + $this->parameters[$key] = $value; + + return $key; + } + + /** + * @inheritDoc + */ + public function merge(array|ParameterInterface ...$arrays): self + { + foreach ($arrays as $array) { + if ($array instanceof ParameterInterface) { + $array = $array->all(); + } + foreach ($array as $key => $value) { + $this->set($key, $value); + } + } + + return $this; + } + + /** + * @inheritDoc + */ + public function get(?string $key = null, mixed $default = null): mixed + { + if ($key === null) { + return $this->parameters; + } + + $key = ':' . ltrim($key, ':'); + if (isset($this->parameters[$key])) { + return $this->parameters[$key]; + } + + return ($default instanceof Closure) ? call_user_func_array($default, []) : $default; + } + + /** + * @inheritDoc + */ + public function all(): array + { + return $this->parameters; + } + + /** + * @inheritDoc + */ + public function reset(): self + { + $this->parameters = []; + + return $this; + } + +} diff --git a/src/QueryBuilder/QueryBuilder.php b/src/QueryBuilder/QueryBuilder.php new file mode 100644 index 0000000..19ee7e7 --- /dev/null +++ b/src/QueryBuilder/QueryBuilder.php @@ -0,0 +1,1455 @@ + [], + 'table' => [], + 'join' => [], + 'where' => [ + 'AND' => [], + 'OR' => [], + ], + 'having' => [ + 'AND' => [], + 'OR' => [], + ], + 'group_by' => [], + 'order_by' => [], + 'offset' => null, + 'limit' => null, + 'set' => [], + 'on' => [ + 'AND' => [], + 'OR' => [], + ], + ]; + + protected array $structure; + + protected ParameterInterface $parameters; + + public function __construct() + { + $this->structure = self::STRUCTURE; + $this->parameters = new Parameters(); + } + + /** + * @return string + * @throws QueryGeneratorException + */ + public function __toString(): string + { + if (empty($this->structure['set'])) { + return $this->generateSelectQuery(); + } + + $isBatch = $this->isBatch(); + $isInsert = empty($this->structure['where']['OR']) && empty($this->structure['where']['AND']) && empty($this->structure['having']['OR']) && empty($this->structure['having']['AND']); + + if ($isInsert) { + return $isBatch ? $this->generateBatchInsertQuery() : $this->generateInsertQuery(); + } + + return $this->generateUpdateQuery(); + } + + /** + * @inheritDoc + */ + public function newBuilder(): self + { + return new self(); + } + + /** + * @param string[]|string|null $ignoreOrCare + * @param null|bool $isIgnore + * @return $this + */ + public function resetStructure(null|array|string $ignoreOrCare = null, ?bool $isIgnore = null): self + { + if ($ignoreOrCare === null) { + $this->structure = self::STRUCTURE; + } else { + if (is_string($ignoreOrCare)) { + $ignoreOrCare = [$ignoreOrCare]; + } + + $newStructure = self::STRUCTURE; + foreach ($ignoreOrCare as $key) { + if (!isset($this->structure[$key])) { + continue; + } + if ($isIgnore) { + $newStructure[$key] = $this->structure[$key]; + } else { + $newStructure[$key] = self::STRUCTURE[$key] ?? []; + } + } + + $this->structure = $newStructure; + } + + return $this; + } + + public function clone(): self + { + return (clone $this); + } + + /** + * @inheritDoc + */ + public function importQB(array $structure, bool $merge = false): self + { + $this->structure = array_merge(($merge ? $this->structure : self::STRUCTURE), $structure); + + return $this; + } + + /** + * @inheritDoc + */ + public function exportQB(): array + { + return $this->structure; + } + + /** + * @inheritDoc + */ + public function getParameter(): ParameterInterface + { + return $this->parameters; + } + + /** + * @inheritDoc + */ + public function setParameter(string $key, mixed $value): self + { + $this->parameters->set($key, $value); + + return $this; + } + + /** + * @inheritDoc + */ + public function setParameters(array $parameters = []): self + { + foreach ($parameters as $key => $value) { + $this->parameters->set($key, $value); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function select(...$columns): self + { + foreach ($columns as $column) { + $column = (string)$column; + $this->structure['select'][] = $column; + } + + return $this; + } + + /** + * @inheritDoc + */ + public function clearSelect(): self + { + $this->structure['select'] = []; + + return $this; + } + + /** + * @inheritDoc + */ + public function selectCount(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'COUNT(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectCountDistinct(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'COUNT(DISTINCT ' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + return $this; + } + + /** + * @inheritDoc + */ + public function selectMax(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'MAX(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectMin(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'MIN(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectAvg(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'AVG(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectAs(RawQuery|string $column, string $alias): self + { + $this->structure['select'][] = $column . ' AS ' . $alias; + + return $this; + } + + /** + * @inheritDoc + */ + public function selectUpper(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'UPPER(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectLower(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'LOWER(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectLength(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'LENGTH(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectMid(RawQuery|string $column, int $offset, int $length, ?string $alias = null): self + { + $this->structure['select'][] = 'MID(' . $column . ', ' . $offset . ', ' . $length . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectLeft(RawQuery|string $column, int $length, ?string $alias = null): self + { + $this->structure['select'][] = 'LEFT(' . $column . ', ' . $length . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectRight(RawQuery|string $column, int $length, ?string $alias = null): self + { + $this->structure['select'][] = 'RIGHT(' . $column . ', ' . $length . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectDistinct(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'DISTINCT(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectCoalesce(RawQuery|string $column, $default = '0', ?string $alias = null): self + { + $this->structure['select'][] = 'COALESCE(' . $column . ', ' . $default . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectSum(RawQuery|string $column, ?string $alias = null): self + { + $this->structure['select'][] = 'SUM(' . $column . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function selectConcat(array $columns, ?string $alias = null): self + { + foreach ($columns as &$column) { + $column = (string)$column; + } + $this->structure['select'][] = 'CONCAT(' . implode(', ', $columns) . ')' + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this; + } + + /** + * @inheritDoc + */ + public function from(RawQuery|string $table, ?string $alias = null): self + { + $this->structure['table'] = []; + + return $this->addFrom($table, $alias); + } + + /** + * @inheritDoc + */ + public function addFrom(RawQuery|string $table, ?string $alias = null): self + { + $table = $table . ($alias !== null ? ' AS ' . $alias : ''); + if (!in_array($table, $this->structure['table'], true)) { + $this->structure['table'][] = $table; + } + + return $this; + } + + /** + * @inheritDoc + */ + public function table(RawQuery|string $table): self + { + $this->structure['table'] = [(string)$table]; + + return $this; + } + + /** + * @inheritDoc + */ + public function groupBy(string|RawQuery|array ...$columns): self + { + foreach ($columns as $column) { + if (is_array($column)) { + $this->groupBy(...$column); + continue; + } + + $column = (string)$column; + if (!in_array($column, $this->structure['group_by'])) { + $this->structure['group_by'][] = $column; + } + } + + return $this; + } + + /** + * @inheritDoc + */ + public function join(RawQuery|string $table, RawQuery|Closure|string $onStmt = null, string $type = 'INNER'): self + { + $table = (string)$table; + + if ($onStmt instanceof Closure) { + $builder = $this->clone()->resetStructure(); + $onStmt = call_user_func_array($onStmt, [&$builder]); + if ($onStmt === null) { + if ($where = $builder->__generateWhereQuery()) { + $this->where($this->raw($where)); + } + if ($having = $builder->__generateHavingQuery()) { + if (str_starts_with($having, ' HAVING ')) { + $having = substr($having, 8); + } + $this->having($this->raw($having)); + } + $onStmt = $builder->__generateOnQuery(); + } + } + + $type = trim(strtoupper($type)); + switch ($type) { + case 'SELF': + $this->addFrom($table); + $this->where(is_string($onStmt) ? $this->raw($onStmt) : $onStmt); + break; + case 'NATURAL': + case 'NATURAL JOIN': + $this->structure['join'][$table] = 'NATURAL JOIN ' . $table; + break; + default: + $this->structure['join'][$table] = trim($type . ' JOIN ' . $table . ' ON ' . $onStmt); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function selfJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self + { + return $this->join($table, $onStmt, 'SELF'); + } + + /** + * @inheritDoc + */ + public function innerJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self + { + return $this->join($table, $onStmt); + } + + /** + * @inheritDoc + */ + public function leftJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self + { + return $this->join($table, $onStmt, 'LEFT'); + } + + /** + * @inheritDoc + */ + public function rightJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self + { + return $this->join($table, $onStmt, 'RIGHT'); + } + + /** + * @inheritDoc + */ + public function leftOuterJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self + { + return $this->join($table, $onStmt, 'LEFT OUTER'); + } + + /** + * @inheritDoc + */ + public function rightOuterJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self + { + return $this->join($table, $onStmt, 'RIGHT OUTER'); + } + + /** + * @inheritDoc + */ + public function naturalJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self + { + return $this->join($table, null, 'NATURAL'); + } + + /** + * @inheritDoc + */ + public function orderBy(RawQuery|string $column, string $soft = 'ASC'): self + { + $soft = trim(strtoupper($soft)); + if (!in_array($soft, ['ASC', 'DESC'], true)) { + throw new InvalidArgumentException('It can only sort as ASC or DESC.'); + } + $orderBy = trim((string)$column) . ' ' . $soft; + + !in_array($orderBy, $this->structure['order_by'], true) && $this->structure['order_by'][] = $orderBy; + + return $this; + } + + /** + * @inheritDoc + */ + public function where(RawQuery|string $column, mixed $operator = '=', mixed $value = null, string $logical = 'AND'): self + { + + $this->whereOrHavingPrepare($operator, $value, $logical); + + $this->structure['where'][$logical][] = $this->whereOrHavingStatementPrepare($column, $operator, $value); + + return $this; + } + + /** + * @inheritDoc + */ + public function having(RawQuery|string $column, mixed $operator = '=', mixed $value = null, string $logical = 'AND'): self + { + $this->whereOrHavingPrepare($operator, $value, $logical); + $this->structure['having'][$logical][] = $this->whereOrHavingStatementPrepare($column, $operator, $value); + + return $this; + } + + /** + * @inheritDoc + */ + public function on(RawQuery|string $column, mixed $operator = '=', mixed $value = null, string $logical = 'AND'): self + { + $this->whereOrHavingPrepare($operator, $value, $logical); + + $this->structure['on'][$logical][] = $this->whereOrHavingStatementPrepare($column, $operator, $value); + + return $this; + } + + /** + * @inheritDoc + */ + public function set(RawQuery|array|string $column, mixed $value = null, bool $strict = true): self + { + return $this->addSet($column, $value, $strict); + } + + /** + * @inheritDoc + */ + public function addSet(RawQuery|array|string $column, mixed $value = null, bool $strict = true): self + { + if (is_array($column) && $value === null) { + $set = []; + foreach ($column as $name => $value) { + $name = (string)$name; + $set[$name] = $this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($name, $value); + } + $this->structure['set'][] = $set; + + return $this; + } + + $column = (string)$column; + $value = $this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value); + + $this->structure['set'][][$column] = $value; + + return $this; + } + + /** + * @inheritDoc + */ + public function andWhere(RawQuery|string $column, mixed $operator = '=', mixed $value = null): self + { + return $this->where($column, $operator, $value); + } + + /** + * @inheritDoc + */ + public function orWhere(RawQuery|string $column, mixed $operator = '=', mixed $value = null): self + { + return $this->where($column, $operator, $value, 'OR'); + } + + /** + * @inheritDoc + */ + public function between(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null, string $logical = 'AND'): self + { + if (is_array($firstValue) && count($firstValue) == 2 && $lastValue === null) { + $value = $firstValue; + } else { + $value = [$firstValue, $lastValue]; + } + + return $this->where($column, 'BETWEEN', $value, $logical); + } + + /** + * @inheritDoc + */ + public function orBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null): self + { + return $this->between($column, [$firstValue, $lastValue], 'OR'); + } + + /** + * @inheritDoc + */ + public function andBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null): self + { + return $this->between($column, $firstValue, $lastValue); + } + + /** + * @inheritDoc + */ + public function notBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null, string $logical = 'AND'): self + { + if (is_array($firstValue) && count($firstValue) == 2 && $lastValue === null) { + $value = $firstValue; + } else { + $value = [$firstValue, $lastValue]; + } + + return $this->where($column, 'NOT BETWEEN', $value, $logical); + } + + /** + * @inheritDoc + */ + public function orNotBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null): self + { + return $this->notBetween($column, $firstValue, $lastValue, 'OR'); + } + + /** + * @inheritDoc + */ + public function andNotBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null): self + { + return $this->notBetween($column, $firstValue, $lastValue); + } + + /** + * @inheritDoc + */ + public function findInSet(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->where($column, 'FIND_IN_SET', $value, $logical); + } + + /** + * @inheritDoc + */ + public function andFindInSet(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'FIND_IN_SET', $value); + } + + /** + * @inheritDoc + */ + public function orFindInSet(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'FIND_IN_SET', $value, 'OR'); + } + + /** + * @inheritDoc + */ + public function notFindInSet(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->where($column, 'NOT FIND_IN_SET', $value, $logical); + } + + /** + * @inheritDoc + */ + public function andNotFindInSet(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'NOT FIND_IN_SET', $value); + } + + /** + * @inheritDoc + */ + public function orNotFindInSet(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'NOT FIND_IN_SET', $value, 'OR'); + } + + /** + * @inheritDoc + */ + public function whereIn(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->where($column, 'IN', $value, $logical); + } + + /** + * @inheritDoc + */ + public function whereNotIn(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->where($column, 'NOT IN', $value, $logical); + } + + /** + * @inheritDoc + */ + public function orWhereIn(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'IN', $value, 'OR'); + } + + /** + * @inheritDoc + */ + public function orWhereNotIn(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'NOT IN', $value, 'OR'); + } + + /** + * @inheritDoc + */ + public function andWhereIn(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'IN', $value); + } + + /** + * @inheritDoc + */ + public function andWhereNotIn(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'IN', $value); + } + + /** + * @inheritDoc + */ + public function regexp(RawQuery|string $column, RawQuery|string $value, string $logical = 'AND'): self + { + return $this->where($column, 'REGEXP', $value, $logical); + } + + /** + * @inheritDoc + */ + public function andRegexp(RawQuery|string $column, RawQuery|string $value): self + { + return $this->where($column, 'REGEXP', $value); + } + + /** + * @inheritDoc + */ + public function orRegexp(RawQuery|string $column, RawQuery|string $value): self + { + return $this->where($column, 'REGEXP', $value, 'OR'); + } + + /** + * @inheritDoc + */ + public function soundex(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->where($column, 'SOUNDEX', $value, $logical); + } + + /** + * @inheritDoc + */ + public function andSoundex(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'SOUNDEX', $value); + } + + /** + * @inheritDoc + */ + public function orSoundex(RawQuery|string $column, mixed $value = null): self + { + return $this->where($column, 'SOUNDEX', $value, 'OR'); + } + + /** + * @inheritDoc + */ + public function whereIsNull(RawQuery|string $column, string $logical = 'AND'): self + { + return $this->where($column, 'IS', null, $logical); + } + + /** + * @inheritDoc + */ + public function orWhereIsNull(RawQuery|string $column): self + { + return $this->where($column, 'IS', null, 'OR'); + } + + /** + * @inheritDoc + */ + public function andWhereIsNull(RawQuery|string $column): self + { + return $this->where($column, 'IS'); + } + + /** + * @inheritDoc + */ + public function whereIsNotNull(RawQuery|string $column, string $logical = 'AND'): self + { + return $this->where($column, 'IS NOT', null, $logical); + } + + /** + * @inheritDoc + */ + public function orWhereIsNotNull(RawQuery|string $column): self + { + return $this->where($column, 'IS NOT', null, 'OR'); + } + + /** + * @inheritDoc + */ + public function andWhereIsNotNull(RawQuery|string $column): self + { + return $this->where($column, 'IS NOT'); + } + + /** + * @inheritDoc + */ + public function offset(int $offset = 0): self + { + $this->structure['offset'] = (int)abs($offset); + + return $this; + } + + /** + * @inheritDoc + */ + public function limit(int $limit): self + { + $this->structure['limit'] = (int)abs($limit); + + return $this; + } + + /** + * @inheritDoc + */ + public function like(RawQuery|array|string $column, mixed $value = null, string $type = 'both', string $logical = 'AND'): self + { + $operator = match (strtolower($type)) { + 'before', 'start' => 'START LIKE', + 'after', 'end' => 'END LIKE', + default => 'LIKE' + }; + + return $this->where($column, $operator, $value, $logical); + } + + /** + * @inheritDoc + */ + public function orLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both'): self + { + return $this->like($column, $value, $type); + } + + /** + * @inheritDoc + */ + public function andLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both'): self + { + return $this->like($column, $value, $type, 'OR'); + } + + /** + * @inheritDoc + */ + public function notLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both', string $logical = 'AND'): self + { + $operator = match (strtolower($type)) { + 'before', 'start' => 'NOT START LIKE', + 'after', 'end' => 'NOT END LIKE', + default => 'NOT LIKE' + }; + + return $this->where($column, $operator, $value, $logical); + } + + /** + * @inheritDoc + */ + public function orNotLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both'): self + { + return $this->notLike($column, $value, $type, 'OR'); + } + + /** + * @inheritDoc + */ + public function andNotLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both'): self + { + return $this->notLike($column, $value, $type); + } + + /** + * @inheritDoc + */ + public function startLike(RawQuery|array|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->like($column, $value, 'before', $logical); + } + + /** + * @inheritDoc + */ + public function orStartLike(RawQuery|array|string $column, mixed $value = null): self + { + return $this->like($column, $value, 'before', 'OR'); + } + + /** + * @inheritDoc + */ + public function andStartLike(RawQuery|array|string $column, mixed $value = null): self + { + return $this->like($column, $value, 'before'); + } + + /** + * @inheritDoc + */ + public function notStartLike(RawQuery|array|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->notLike($column, $value, 'before', $logical); + } + + /** + * @inheritDoc + */ + public function orStartNotLike(RawQuery|array|string $column, mixed $value = null): self + { + return $this->notLike($column, $value, 'before', 'OR'); + } + + /** + * @inheritDoc + */ + public function andStartNotLike(RawQuery|array|string $column, mixed $value = null): self + { + return $this->notLike($column, $value, 'before'); + } + + /** + * @inheritDoc + */ + public function endLike(RawQuery|array|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->like($column, $value, 'after', $logical); + } + + /** + * @inheritDoc + */ + public function orEndLike(RawQuery|array|string $column, mixed $value = null): self + { + return $this->like($column, $value, 'after', 'OR'); + } + + /** + * @inheritDoc + */ + public function andEndLike(RawQuery|array|string $column, mixed $value = null): self + { + return $this->like($column, $value, 'after'); + } + + /** + * @inheritDoc + */ + public function notEndLike(RawQuery|array|string $column, mixed $value = null, string $logical = 'AND'): self + { + return $this->notLike($column, $value, 'after', $logical); + } + + /** + * @inheritDoc + */ + public function orEndNotLike(RawQuery|array|string $column, mixed $value = null): self + { + return $this->notLike($column, $value, 'after', 'OR'); + } + + /** + * @inheritDoc + */ + public function andEndNotLike(RawQuery|array|string $column, mixed $value = null): self + { + return $this->notLike($column, $value, 'after'); + } + + /** + * @inheritDoc + */ + public function subQuery(Closure $closure, ?string $alias = null, bool $isIntervalQuery = true): RawQuery + { + $builder = $this->clone()->resetStructure(); + + call_user_func_array($closure, [&$builder]); + if ($alias !== null && $isIntervalQuery !== TRUE) { + throw new QueryBuilderException('To define alias to a subquery, it must be an inner query.'); + } + + $rawQuery = ($isIntervalQuery ? '(' : '') + . $builder->generateSelectQuery() + . ($isIntervalQuery ? ')' : '') + . ($alias !== null ? ' AS ' . $alias : ''); + + return $this->raw($rawQuery); + } + + /** + * @inheritDoc + */ + public function group(Closure $closure, string $logical = 'AND'): self + { + $logical = str_replace(['&&', '||'], ['AND', 'OR'], strtoupper($logical)); + if(!in_array($logical, ['AND', 'OR'], true)){ + throw new QueryBuilderException('Logical operator OR, AND, && or || it could be.'); + } + + $builder = $this->clone(); + call_user_func_array($closure, [$builder->resetStructure()]); + + + + foreach (['where', 'on', 'having'] as $stmt) { + $statement = $builder->__generateStructure($stmt); + !empty($statement) && $this->structure[$stmt][$logical][] = '(' . $statement . ')'; + } + + return $this; + } + + /** + * @inheritDoc + */ + public function raw(mixed $rawQuery): RawQuery + { + return new RawQuery($rawQuery); + } + + /** + * @return string + * @throws QueryGeneratorException + */ + public function generateInsertQuery(): string + { + $columns = []; + $values = []; + $set = array_merge(...$this->structure['set']); + foreach ($set as $column => $value) { + $columns[] = $column; + $values[] = $value; + } + if (empty($columns)) { + throw new QueryGeneratorException('The data set for the insert could not be found.'); + } + + return 'INSERT INTO' + . ' ' . $this->__generateSchemaName() . ' ' + . '(' . implode(', ', $columns) . ')' + . ' VALUES ' + . '(' . implode(', ', $values) . ');'; + } + + /** + * @return string + * @throws QueryGeneratorException + */ + public function generateBatchInsertQuery(): string + { + $columns = array_keys(array_merge(...$this->structure['set'])); + if (empty($columns)) { + throw new QueryGeneratorException('The data set for the insert could not be found.'); + } + $values = []; + foreach ($this->structure['set'] as $set) { + $value = []; + foreach ($columns as $column) { + $value[$column] = $set[$column] ?? 'NULL'; + } + $values[] = '(' . implode(', ', $value) . ')'; + } + + return 'INSERT INTO' + . ' ' . $this->__generateSchemaName() . ' ' + . '(' . implode(', ', $columns) . ')' + . ' VALUES ' + . implode(', ', $values) . ';'; + } + + /** + * @return string + * @throws QueryGeneratorException + */ + public function generateDeleteQuery(): string + { + return 'DELETE FROM' + . ' ' + . $this->__generateSchemaName() + . ' WHERE ' + . (($where = $this->__generateWhereQuery()) !== null ? $where : '1') + . ($this->__generateLimitQuery() ?? ''); + } + + /** + * @param array $selector + * @param array $conditions + * @return string + */ + public function generateSelectQuery(array $selector = [], array $conditions = []): string + { + !empty($selector) && $this->select(...$selector); + if (!empty($conditions)) { + foreach ($conditions as $column => $value) { + if (is_string($column)) { + $this->where($column, $value); + } else { + $this->where($value); + } + } + } + + return 'SELECT ' + . (empty($this->structure['select']) ? '*' : implode(', ', $this->structure['select'])) + . ' FROM ' + . implode(', ', $this->structure['table']) + . (!empty($this->structure['join']) ? ' ' . implode(' ', $this->structure['join']) : '') + . ' WHERE ' + . (($where = $this->__generateWhereQuery()) ? $where : '1') + . (!empty($this->structure['group_by']) ? ' GROUP BY ' . implode(', ', $this->structure['group_by']) : '') + . ($this->__generateHavingQuery() ?? '') + . (!empty($this->structure['order_by']) ? ' ORDER BY ' . implode(', ', $this->structure['order_by']) : '') + . ($this->__generateLimitQuery() ?? ''); + } + + /** + * @return string + * @throws QueryGeneratorException + */ + public function generateUpdateQuery(): string + { + $set = array_merge(...$this->structure['set']); + $updateSet = []; + foreach ($set as $column => $value) { + $updateSet[] = $column . ' = ' . $value; + } + if (empty($updateSet)) { + throw new QueryGeneratorException('The data set for the insert could not be found.'); + } + + return 'UPDATE ' . $this->__generateSchemaName() + . ' SET ' . implode(', ', $updateSet) + . ' WHERE ' + . (($where = $this->__generateWhereQuery()) ? $where : '1') + . ($this->__generateHavingQuery() ?? '') + . ($this->__generateLimitQuery() ?? ''); + } + + /** + * @param string $referenceColumn + * @return string + * @throws QueryGeneratorException + */ + public function generateUpdateBatchQuery(string $referenceColumn): string + { + $update = []; + $data = $this->structure['set']; + $updateData = $columns = $where = []; + foreach ($data as $set) { + if (!isset($set[$referenceColumn])) { + throw new QueryGeneratorException('The reference column does not exist in one or more of the set arrays.'); + } + $setData = []; + $where[] = $set[$referenceColumn]; + unset($set[$referenceColumn]); + foreach ($set as $key => $value) { + $setData[$key] = $value; + (!in_array($key, $columns)) && $columns[] = $key; + } + $updateData[] = $setData; + } + foreach ($columns as $column) { + $syntax = $column . ' = CASE'; + foreach ($updateData as $key => $values) { + if (!array_key_exists($column, $values)) { + continue; + } + $syntax .= ' WHEN ' . $referenceColumn . ' = ' + . ($this->isSQLParameterOrFunction($where[$key]) ? $where[$key] : $this->parameters->add($referenceColumn, $where[$key])) + . ' THEN ' + . $values[$column]; + } + $update[] = $syntax . ' ELSE ' . $column .' END'; + } + + $this->whereIn($referenceColumn, $where); + + return 'UPDATE ' . $this->__generateSchemaName() + . ' SET ' + . implode(', ', $update) + . ' WHERE ' + . (($where = $this->__generateWhereQuery()) ? $where : '1') + . ($this->__generateHavingQuery() ?? '') + . ($this->__generateLimitQuery() ?? ''); + } + + protected function isSQLParameter($value): bool + { + return (is_string($value)) && ($value === '?' || preg_match('/^:[(\w)]+$/', $value)); + } + + protected function isSQLParameterOrFunction($value): bool + { + return ((is_string($value)) && ( + $value === '?' + || preg_match('/^:[(\w)]+$/', $value) + || preg_match('/^[a-zA-Z_]+[.]+[a-zA-Z_]+$/', $value) + || preg_match('/^[a-zA-Z_]+\(\)$/', $value) + )) || ($value instanceof RawQuery) || is_int($value); + } + + public function isBatch(): bool + { + foreach ($this->structure['set'] as $set) { + if (is_array($set) && count($set) > 1) { + return true; + } + } + + return false; + } + + private function whereOrHavingStatementPrepare($column, $operator, $value): string + { + $operator = trim($operator); + $column = (string)$column; + + if ($value !== null && in_array($operator, [ + '=', '!=', '>', '<', '>=', '<=', '<>', + '+', '-', '*', '/', '%', + '+=', '-=', '*=', '/=', '%=', '&=', '^-=', '|*=' + ], true)) { + return $column . ' ' . $operator . ' ' + . ($this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value)); + } + $upperCaseOperator = strtoupper($operator); + $searchOperator = str_replace([' ', '_'], '', $upperCaseOperator); + if ($value === null && !in_array($searchOperator, ['IS', 'ISNOT'])) { + return $column; + } + + switch ($searchOperator) { + case 'IS': + return $column . ' IS ' + . ((($value === null) ? 'NULL' : ($this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value)))); + case 'ISNOT': + return $column . ' IS NOT ' + . ((($value === null) ? 'NULL' : ($this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value)))); + case 'LIKE': + case 'NOTLIKE': + case 'STARTLIKE': + case 'NOTSTARTLIKE': + case 'ENDLIKE': + case 'NOTENDLIKE': + if (!$this->isSQLParameter($value)) { + $value = (in_array($searchOperator, ['LIKE', 'NOTLIKE', 'STARTLIKE', 'NOTSTARTLIKE']) ? '%' : '') + . $value + . (in_array($searchOperator, ['LIKE', 'NOTLIKE', 'ENDLIKE', 'NOTENDLIKE']) ? '%' : ''); + + $value = $this->parameters->add($column, $value); + } + + return $column + . (in_array($searchOperator, ['NOTSTARTLIKE', 'NOTLIKE', 'NOTENDLIKE']) ? ' NOT' : '') + . ' LIKE ' . $value; + case 'BETWEEN': + case 'NOTBETWEEN': + return $column . ' ' + . ($searchOperator === 'NOTBETWEEN' ? 'NOT ' : '') + . 'BETWEEN ' + . ($this->isSQLParameterOrFunction($value[0]) ? $value[0] : $this->parameters->add($column, $value[0])) + . ' AND ' + . ($this->isSQLParameterOrFunction($value[1]) ? $value[1] : $this->parameters->add($column, $value[1])); + case 'IN': + case 'NOTIN': + if (is_array($value)) { + $values = []; + array_map(function ($item) use (&$values, $column) { + if (is_numeric($item)) { + $values[] = $item; + } else { + $values[] = $this->isSQLParameterOrFunction($item) ? $item : $this->parameters->add($column, $item); + } + }, array_unique($value)); + $value = '(' . implode(', ', $values) . ')'; + } + return $column + . ($searchOperator === 'NOTIN' ? ' NOT' : '') + . ' IN ' . $value; + case 'REGEXP': + return $column . ' REGEXP ' + . ($this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value)); + case 'FINDINSET': + case 'NOTFINDINSET': + if (is_array($value)) { + $value = implode(', ', $value); + } elseif ($this->isSQLParameterOrFunction($value)) { + $value = $this->parameters->add($column, $value); + } + return ($searchOperator === 'NOTFINDINSET' ? 'NOT ' : '') + . 'FIND_IN_SET(' . $value . ', ' . $column . ')'; + case 'SOUNDEX': + if (!$this->isSQLParameterOrFunction($value)) { + $value = $this->parameters->add($column, $value); + } + return "SOUNDEX(" . $column . ") LIKE CONCAT('%', TRIM(TRAILING '0' FROM SOUNDEX(" . $value . ")), '%')"; + default: + if ($value === null && preg_match('/([\w_]+)\((.+)\)$/iu', $column, $matches) !== FALSE) { + return strtoupper($matches[1]) . '(' . $matches[2] . ')'; + } + return $column . ' ' . $operator . ' ' . $this->parameters->add($column, $value); + } + } + + /** + * @return string + * @throws QueryGeneratorException + */ + private function __generateSchemaName(): string + { + if (!empty($this->structure['table'])) { + $table = end($this->structure['table']); + } else { + throw new QueryGeneratorException('Table name not found when query.'); + } + + return $table; + } + + private function __generateLimitQuery(): ?string + { + if ($this->structure['limit'] === null && $this->structure['offset'] === null) { + return null; + } + $statement = ' '; + if ($this->structure['limit'] === null) { + $statement .= 'OFFSET ' . $this->structure['offset']; + } else { + $statement .= 'LIMIT ' + . ($this->structure['offset'] !== null ? $this->structure['offset'] . ', ' : '') + . $this->structure['limit']; + } + + return $statement; + } + + private function __generateOnQuery(): ?string + { + return $this->__generateStructure('on'); + } + + private function __generateHavingQuery(): ?string + { + $stmt = $this->__generateStructure('having'); + + return $stmt === null ? null : ' HAVING ' . $stmt; + } + + private function __generateWhereQuery(): ?string + { + return $this->__generateStructure('where'); + } + + private function __generateStructure(string $key): ?string + { + $isAndEmpty = empty($this->structure[$key]['AND']); + $isOrEmpty = empty($this->structure[$key]['OR']); + if ($isOrEmpty && $isAndEmpty) { + return null; + } + + return (!$isAndEmpty ? implode(' AND ', $this->structure[$key]['AND']) : '') + . (!$isAndEmpty && !$isOrEmpty ? ' AND ' : '') + . (!$isOrEmpty ? implode(' OR ', $this->structure[$key]['OR']) : ''); + } + + private function whereOrHavingPrepare(&$operator, &$value, &$logical): void + { + $logical = strtoupper(strtr($logical, [ + '&&' => 'AND', + '||' => 'OR', + ])); + if (!in_array($logical, ['AND', 'OR'], true)) { + throw new InvalidArgumentException('Logical operator OR, AND, && or || it could be.'); + } + + if ($value === null && !in_array($operator, [ + 'IS', 'IS NOT', + '=', '!=', '>', '<', '>=', '<=', '<>', + '+', '-', '*', '/', '%', + '+=', '-=', '*=', '/=', '%=', '&=', '^-=', '|*=' + ])) { + $value = $operator; + $operator = '='; + } + } + +} diff --git a/src/QueryBuilder/RawQuery.php b/src/QueryBuilder/RawQuery.php new file mode 100644 index 0000000..85b41f3 --- /dev/null +++ b/src/QueryBuilder/RawQuery.php @@ -0,0 +1,53 @@ +set($rawQuery); + } + + public function __toString(): string + { + return $this->get(); + } + + public function set(mixed $rawQuery): self + { + if (is_string($rawQuery)) { + $this->raw = $rawQuery; + } else if ($rawQuery instanceof Closure) { + $builder = new QueryBuilder(); + $res = call_user_func_array($rawQuery, [&$builder]); + if (is_string($res)) { + $this->raw = $res; + } else if (is_object($res) && method_exists($res, '__toString')) { + $this->raw = $res->__toString(); + } else { + $this->raw = $builder->__toString(); + } + } else { + $this->raw = (string)$rawQuery; + } + + return $this; + } + + public function get(): string + { + return $this->raw ?? ''; + } + + public static function raw($rawQuery): RawQuery + { + return new self($rawQuery); + } + +} diff --git a/src/Raw.php b/src/Raw.php deleted file mode 100644 index 66709dc..0000000 --- a/src/Raw.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database; - -final class Raw -{ - - private string $raw; - - public function __construct(string $rawQuery) - { - $this->raw = \trim($rawQuery); - } - - public function __toString(): string - { - return $this->get(); - } - - public function get(): string - { - return $this->raw; - } - - public static function raw(string $sqlQuery): self - { - return new self($sqlQuery); - } - -} diff --git a/src/Result.php b/src/Result.php deleted file mode 100644 index 02d855f..0000000 --- a/src/Result.php +++ /dev/null @@ -1,180 +0,0 @@ - - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database; - -final class Result -{ - - private \PDOStatement $statement; - - private string $query; - - private int $num_rows; - - public function __construct(\PDOStatement $statement) - { - $this->setStatement($statement); - } - - public function setStatement(\PDOStatement $statement): self - { - $this->statement = $statement; - $this->query = $statement->queryString; - $this->num_rows = (int)$statement->rowCount(); - return $this; - } - - public function withStatement(\PDOStatement $statement): self - { - $with = clone $this; - return $with->setStatement($statement); - } - - public function getStatement(): \PDOStatement - { - return $this->statement; - } - - public function numRows(): int - { - return $this->num_rows; - } - - public function rowCount(): int - { - return $this->numRows(); - } - - public function query(): string - { - return $this->query; - } - - /** - * @param string $entityClass - * @return array|object|null - */ - public function toEntity(string $entityClass = Entity::class) - { - if($this->num_rows === 0){ - return null; - } - $this->asEntity($entityClass); - return $this->num_rows === 1 ? $this->getStatement()->fetch() : $this->getStatement()->fetchAll(); - } - - public function asEntity(string $entityClass = Entity::class): self - { - $this->getStatement()->setFetchMode(\PDO::FETCH_CLASS, $entityClass); - return $this; - } - - public function toAssoc(): array - { - if($this->num_rows === 0){ - return []; - } - $this->asAssoc(); - return $this->num_rows === 1 ? $this->getStatement()->fetch() : $this->getStatement()->fetchAll(); - } - - public function asAssoc(): self - { - $this->getStatement()->setFetchMode(\PDO::FETCH_ASSOC); - return $this; - } - - public function toArray(): array - { - if($this->num_rows === 0){ - return []; - } - $this->asArray(); - return $this->num_rows === 1 ? $this->getStatement()->fetch() : $this->getStatement()->fetchAll(); - } - - public function asArray(): self - { - $this->getStatement()->setFetchMode(\PDO::FETCH_BOTH); - return $this; - } - - /** - * @return object|array|null - */ - public function toObject() - { - if($this->num_rows === 0){ - return null; - } - $this->asObject(); - return $this->num_rows === 1 ? $this->getStatement()->fetch() : $this->getStatement()->fetchAll(); - } - - public function asObject(): self - { - $this->getStatement()->setFetchMode(\PDO::FETCH_OBJ); - return $this; - } - - public function toLazy() - { - if($this->num_rows === 0){ - return null; - } - $this->asLazy(); - return $this->num_rows === 1 ? $this->getStatement()->fetch() : $this->getStatement()->fetchAll(); - } - - public function asLazy(): self - { - $this->getStatement()->setFetchMode(\PDO::FETCH_LAZY); - return $this; - } - - public function result() - { - if($this->num_rows <= 0){ - return null; - } - $res = $this->getStatement()->fetch(); - - return $res !== FALSE ? $res : null; - } - - public function row() - { - return $this->result(); - } - - /** - * @return array|object[]|null - */ - public function results(): ?array - { - if($this->num_rows <= 0){ - return null; - } - $res = $this->getStatement()->fetchAll(); - - return $res !== FALSE ? $res : null; - } - - public function rows(): ?array - { - return $this->results(); - } - - -} diff --git a/src/Utils/Datatables.php b/src/Utils/Datatables.php index d689cfc..32eb8ed 100644 --- a/src/Utils/Datatables.php +++ b/src/Utils/Datatables.php @@ -13,173 +13,270 @@ namespace InitPHP\Database\Utils; -use InitPHP\Database\Database; +use \InitPHP\Database\DBAL\Interfaces\{DatabaseInterface, CRUDInterface}; +use InitPHP\Database\DBAL\Exceptions\SQLQueryExecuteException; +use InitPHP\Database\ORM\Interfaces\ModelInterface; +use InitPHP\Database\QueryBuilder\Interfaces\QueryBuilderInterface; +use Closure; -final class Datatables +/** + * @mixin QueryBuilderInterface + */ +class Datatables { - public const GET_REQUEST = 0; - - public const POST_REQUEST = 1; + private DatabaseInterface|CRUDInterface|ModelInterface $db; - private Database $db; + private array $request; - private array $request = []; + private array $response = [ + 'draw' => 0, + 'recordsTotal' => 0, + 'recordsFiltered' => 0, + 'data' => [], + 'post' => [], + ]; private array $columns = []; - private int $total_row = 0; + private array $renders = []; - private int $total_filtered_row = 0; + private array $builder = []; - private array $results; + private bool $orderByReset = true; - private int $draw = 0; + private array $permanentSelect = []; - public function __construct(Database $db, $columns, $method = self::GET_REQUEST) + public function __construct(DatabaseInterface|ModelInterface|CRUDInterface $db) { - $this->db = $db; + $this->request = array_merge($_GET ?? [], $_POST ?? []); - switch ($method) { - case self::GET_REQUEST: - $this->request = $_GET ?? []; - break; - case self::POST_REQUEST: - $this->request = $_POST ?? []; - break; + if ($requestBody = @file_get_contents("php://input")) { + if (is_array($jsonBody = json_decode($requestBody, true))) { + $this->request = array_merge($this->request, $jsonBody); + } } - $this->columns = $columns; - - $this->total_row = $this->db->count(); + $this->db = $db; + } - $this->filterQuery(); - $this->orderQuery(); - $this->total_filtered_row = $this->db->count(); - $this->limitQuery(); - $this->results = $this->db->get()->toArray(); + public function __call(string $name, array $arguments) + { + $this->builder[] = [ + 'method' => $name, + 'arguments' => $arguments, + ]; - if(isset($this->request['draw'])){ - $this->draw = (int)$this->request['draw']; - } + return $this; } + /** + * @return string + * @throws SQLQueryExecuteException + */ public function __toString(): string { - return \json_encode($this->getResults()); + return json_encode($this->toArray()); } - public function getResults(): array + /** + * @return array + * @throws SQLQueryExecuteException + */ + public function toArray(): array { - return [ - 'draw' => $this->draw, - 'recordsTotal' => $this->total_row, - 'recordsFiltered' => $this->total_filtered_row, - 'data' => $this->output_prepare() - ]; + $this->handle(); + return $this->response; } + /** + * @return $this + * @throws SQLQueryExecuteException + */ + public function handle(): self + { + $this->filterQuery(); + + $totalRow = $this->getCount(); - private function orderQuery() + $res = $this->orderQuery() + ->limitQuery() + ->getResults(); + + if (!empty($this->renders)) { + foreach ($res as &$row) { + foreach ($row as $column => &$value) { + if (!isset($this->renders[$column])) { + continue; + } + $value = call_user_func_array($this->renders[$column], [$value, &$row]); + } + } + } + + $this->response = [ + 'draw' => $this->request['draw'] ?? 0, + 'recordsTotal' => $totalRow, + 'recordsFiltered' => $totalRow, + 'data' => $res, + 'post' => $this->request, + ]; + + return $this; + } + + /** + * @param string ...$select + * @return $this + */ + public function addPermanentSelect(string ...$select): self { - $columns = $this->columns; - if(!isset($this->request['order'])){ - return; + foreach ($select as $sel) { + $this->select($sel); + $this->permanentSelect[] = $sel; } - $count = \count($this->request['order']); - $dtColumns = $this->pluck($columns, 'dt'); - for($i = 0; $i < $count; ++$i){ - $columnId = \intval($this->request['order'][$i]['column']); - $reqColumn = $this->request['columns'][$columnId]; - $columnId = \array_search($reqColumn['data'], $dtColumns); - $column = $columns[$columnId]; - if(($reqColumn['orderable'] ?? 'false') != 'true' || !isset($column['db'])){ - continue; - } - $dir = ($this->request['order'][$i]['dir'] ?? 'asc') === 'asc' ? 'ASC' : 'DESC'; - $this->db->orderBy($column['db'], $dir); + + return $this; + } + + /** + * @param string|null ...$columns + * @return $this + */ + public function setColumns(?string ...$columns): self + { + $dt = count($this->columns) - 1; + foreach ($columns as $column) { + $this->columns[] = [ + 'db' => $column, + 'dt' => ++$dt, + ]; } + + return $this; } - private function filterQuery() + /** + * @param string $column + * @param Closure $render + * @return $this + */ + public function addRender(string $column, Closure $render): self { - $columns = $this->columns; - $dtColumns = $this->pluck($columns, 'dt'); - $str = $this->request['search']['value'] ?? ''; - if($str === '' || !isset($this->request['columns'])){ - return; + $this->renders[$column] = $render; + + return $this; + } + + /** + * @return $this + */ + public function orderBySave(): self + { + $this->orderByReset = false; + + return $this; + } + + /** + * @return int + * @throws SQLQueryExecuteException + */ + private function getCount(): int + { + if (!empty($this->permanentSelect)) { + foreach ($this->permanentSelect as $select) { + $this->db->select($select); + } } - $columnsCount = \count($this->request['columns']); - $this->db->group(function (Database $db) use ($str, $dtColumns, $columns, $columnsCount) { - for ($i = 0; $i < $columnsCount; ++$i) { - $reqColumn = $this->request['columns'][$i]; - $columnId = \array_search($reqColumn['data'], $dtColumns); - $column = $columns[$columnId]; - if(empty($column['db'])){ - continue; - } - $db->orLike($column['db'], $str); + $isGroupBy = false; + foreach ($this->builder as $process) { + if (str_starts_with($process['method'], 'select') || str_starts_with($process['method'], 'orderBy')) { + continue; } - }); - if(isset($this->request['columns'])){ - for ($i = 0; $i < $columnsCount; ++$i) { - $reqColumn = $this->request['columns'][$i]; - $columnId = \array_search($reqColumn['data'], $dtColumns); - $column = $columns[$columnId]; - $str = $reqColumn['search']['value'] ?? ''; - if(($reqColumn['searchable'] ?? 'false') != 'true' || $str == '' || empty($column['db'])){ - continue; + if ($process['method'] === 'groupBy') { + if ($isGroupBy === false) { + $this->db->selectCountDistinct(current($process['arguments']), 'data_length'); + $isGroupBy = true; } - $this->db->like($column['db'], $str); + continue; } + $this->db->{$process['method']}(...$process['arguments']); } + $isGroupBy === false && $this->db->selectCount('*', 'data_length'); + $res = $this->db->get(); + + return $res->numRows() > 0 ? $res->asAssoc()->row()['data_length'] : 0; } - private function limitQuery() + /** + * @return array + * @throws SQLQueryExecuteException + */ + private function getResults(): array { - if(isset($this->request['start']) && $this->request['length'] != -1){ - $this->db->offset((int)$this->request['start']) - ->limit((int)$this->request['length']); + foreach ($this->builder as $process) { + $this->db->{$process['method']}(...$process['arguments']); } + $res = $this->db->get(); + + return $res->numRows() > 0 ? $res->asAssoc()->rows() : []; } - private function output_prepare(): array + /** + * @return self + */ + private function orderQuery(): self { - $out = []; - $columns = $this->columns; - $data = $this->results; - $dataCount = \count($data); - $columnCount = \count($columns); - - for ($i = 0; $i < $dataCount; ++$i) { - $row = []; - - for ($y = 0; $y < $columnCount; ++$y) { - $column = $columns[$y]; - if(isset($column['formatter'])){ - $row[$column['dt']] = \call_user_func_array($column['formatter'], (empty($column['db']) ? [$data[$i]] : [$data[$i][$column['db']], $data[$i]])); - }else{ - $row[$column['dt']] = !empty($column['db']) ? $data[$i][$column['db']] : ''; + if (empty($this->request['order']) || !is_array($this->request['order'])) { + return $this; + } + if ($this->orderByReset) { + foreach ($this->builder as $key => $builder) { + if (str_starts_with($builder['method'], 'orderBy')) { + unset($this->builder[$key]); } } - - $out[] = $row; + } + $columns = $this->columns; + $count = count($this->request['order']); + for ($i = 0; $i < $count; ++$i) { + $columnId = intval($this->request['order'][$i]['column']); + $column = $columns[$columnId]; + if (!isset($column['db'])) { + continue; + } + $dir = strtolower($this->request['order'][$i]['dir']) === 'asc' ? 'ASC' : 'DESC'; + $this->db->orderBy($this->db->raw($column['db']), $dir); } - - return $out; + return $this; } - private function pluck(array $array, string $prop): array + /** + * @return void + */ + private function filterQuery(): void { - $out = []; - $len = \count($array); - for ($i = 0; $i < $len; ++$i) { - if(empty($array[$i][$prop])){ - continue; + $search = $this->request['search']['value'] ?? null; + if (empty($search) || empty($this->columns)) { + return; + } + $this->db->group(function (QueryBuilderInterface $builder) use ($search) { + foreach ($this->columns as $column) { + $builder->orLike($this->db->raw($column['db']), $search); } - $out[$i] = $array[$i][$prop]; + return $builder; + }); + } + + private function limitQuery(): self + { + if (isset($this->request['start']) && $this->request['length'] != -1) { + $this->offset($this->request['start']) + ->limit($this->request['length']); } - return $out; + + return $this; } } diff --git a/src/Utils/Helper.php b/src/Utils/Helper.php new file mode 100644 index 0000000..42076b9 --- /dev/null +++ b/src/Utils/Helper.php @@ -0,0 +1,37 @@ + - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 2.0.7 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Utils; - -use InitPHP\Database\Result; - -/** - * @mixin Result - */ -final class Pagination -{ - - private Result $result; - - protected int $page = 1; - protected int $perPageLimit = 10; - protected int $totalPage = 0; - protected int $totalRow = 0; - protected int $howDisplayedPage = 8; - protected string $linkTemplate = ''; - - public function __construct(Result &$res, int $page, int $perPageLimit, int $totalRow, string $linkTemplate) - { - $this->result = &$res; - - $this->page = $page; - $this->perPageLimit = $perPageLimit; - $this->totalRow = $totalRow; - $this->linkTemplate = $linkTemplate; - - $this->totalPage = \ceil(($this->totalRow / $this->perPageLimit)); - } - - public function __call($name, $arguments) - { - return $this->getResult()->{$name}(...$arguments); - } - - public function getResult(): Result - { - return $this->result; - } - - public function getPage(): int - { - return $this->page; - } - - public function getTotalPage(): int - { - return $this->totalPage; - } - - public function getTotalRow(): int - { - return $this->totalRow; - } - - public function setDisplayedPage(int $displayedPage): self - { - $this->howDisplayedPage = ($displayedPage % 2 !== 0) ? $displayedPage + 1 : $displayedPage; - - return $this; - } - - public function getDisplayedPage(): int - { - return $this->howDisplayedPage; - } - - public function setBaseLink($link): self - { - $this->linkTemplate = $link; - - return $this; - } - - public function getPages(): array - { - $pages = []; - $beforeAfter = $this->howDisplayedPage / 2; - $beforeLimit = ($beforeAfter > $this->page) ? ($beforeAfter - ($beforeAfter - $this->page)) : $beforeAfter; - $afterLimit = $this->howDisplayedPage - $beforeLimit; - - for ($i = ($this->page - 1); $i >= ($this->page - $beforeLimit); --$i) { - $pages[] = [ - 'url' => $this->_linkGenerator($i), - 'page' => $i, - 'active' => false, - ]; - } - $pages = \array_reverse($pages, false); - - $pages[] = [ - 'url' => $this->_linkGenerator($this->page), - 'page' => $this->page, - 'active' => true, - ]; - - for ($i = ($this->page + 1); $i <= ($this->page + $afterLimit); ++$i) { - if($i > $this->totalPage){ - break; - } - $pages[] = [ - 'url' => $this->_linkGenerator($i), - 'page' => $i, - 'active' => false, - ]; - } - - return $pages; - } - - public function nextPage(): ?array - { - if($this->page < $this->totalPage && $this->totalPage > 1){ - $page = $this->page + 1; - return [ - 'url' => $this->_linkGenerator($page), - 'page' => $page, - ]; - } - return null; - } - - public function prevPage(): ?array - { - if($this->page > 1){ - $page = $this->page - 1; - return [ - 'url' => $this->_linkGenerator($page), - 'page' => $page, - ]; - } - return null; - } - - public function toHTML(array $attrs = []): string - { - $pages = $this->getPages(); - $prev = $this->prevPage(); - $next = $this->nextPage(); - $li_attr_prepare = ''; - $li_a_attr_prepare = ''; - $li_attr_prepare_active = ' class="active"'; - if(isset($attrs['li'])){ - $li_attr_prepare = $this->_attrGenerator($attrs['li']); - $li_a_attr_prepare = $this->_attrGenerator(($attrs['li']['a'] ?? [])); - if(isset($attrs['li']['class'])){ - $attrs['li']['class'] .= ' active'; - }else{ - $attrs['li']['class'] = 'active'; - } - $li_attr_prepare_active = $this->_attrGenerator($attrs['li']); - } - - $html = '_attrGenerator(($attrs['nav'] ?? [])) - . '>_attrGenerator(($attrs['ul'] ?? [])) - . '>'; - if($prev !== null){ - $html .= '' . ($attrs['prev_label'] ?? '«') . ''; - } - - foreach ($pages as $page) { - $html .= '' - . $page['page'] - . ''; - } - - if($next !== null){ - $html .= '' . ($attrs['next_label'] ?? '»') . ''; - } - - $html .= ''; - return $html; - } - - - private function _attrGenerator(array $assoc): string - { - if($assoc === []){ - return ''; - } - $res = ''; - foreach ($assoc as $name => $value) { - if(\is_array($value)){ - continue; - } - $res .= ' ' . $name . '="' . $value . '"'; - } - return $res; - } - - - private function _linkGenerator($page) - { - if(empty($this->linkTemplate)){ - return $page; - } - return \strtr($this->linkTemplate, [ - '{page}' => $page, - ]); - } - -} diff --git a/tests/QueryBuilderUnitTest.php b/tests/QueryBuilderUnitTest.php index ee1bc94..4aaade8 100644 --- a/tests/QueryBuilderUnitTest.php +++ b/tests/QueryBuilderUnitTest.php @@ -3,18 +3,16 @@ namespace Test\InitPHP\Database; -use InitPHP\Database\Database; -use InitPHP\Database\Helpers\Parameters; -use InitPHP\Database\QueryBuilder; +use InitPHP\Database\QueryBuilder\QueryBuilder; class QueryBuilderUnitTest extends \PHPUnit\Framework\TestCase { - protected Database $db; + protected QueryBuilder $db; protected function setUp(): void { - $this->db = new Database(); + $this->db = new QueryBuilder(); parent::setUp(); } @@ -26,7 +24,7 @@ public function testSelectBuilder() $expected = "SELECT id, name FROM user WHERE 1"; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testBlankBuild() @@ -36,7 +34,7 @@ public function testBlankBuild() $expected = 'SELECT * FROM post WHERE 1'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testSelfJoinBuild() @@ -48,7 +46,7 @@ public function testSelfJoinBuild() $expected = "SELECT post.id, post.title, user.name AS authorName FROM post, user WHERE user.id = post.user"; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testInnerJoinBuild() @@ -60,7 +58,7 @@ public function testInnerJoinBuild() $expected = "SELECT post.id, post.title, user.name as authorName FROM post INNER JOIN user ON user.id = post.user WHERE 1"; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testLeftJoinBuild() @@ -72,7 +70,7 @@ public function testLeftJoinBuild() $expected = "SELECT post.id, post.title, user.name as authorName FROM post LEFT JOIN user ON user.id=post.user WHERE 1"; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testRightJoinBuild() @@ -84,7 +82,7 @@ public function testRightJoinBuild() $expected = "SELECT post.id, post.title, user.name as authorName FROM post RIGHT JOIN user ON user.id=post.user WHERE 1"; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testLeftOuterJoinBuild() @@ -96,7 +94,7 @@ public function testLeftOuterJoinBuild() $expected = "SELECT post.id, post.title, user.name as authorName FROM post LEFT OUTER JOIN user ON user.id=post.user WHERE 1"; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testRightOuterJoinBuild() @@ -108,7 +106,7 @@ public function testRightOuterJoinBuild() $expected = "SELECT post.id, post.title, user.name as authorName FROM post RIGHT OUTER JOIN user ON user.id=post.user WHERE 1"; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testLimitStatement() @@ -120,7 +118,7 @@ public function testLimitStatement() $expected = 'SELECT id FROM book WHERE 1 LIMIT 5'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testOffsetStatement() @@ -129,11 +127,10 @@ public function testOffsetStatement() ->from('book') ->offset(5); - // Offset is specified If no limit is specified; The limit is 10000. - $expected = 'SELECT id FROM book WHERE 1 LIMIT 5, 10000'; + $expected = 'SELECT id FROM book WHERE 1 OFFSET 5'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testOffsetLimitStatement() @@ -146,7 +143,7 @@ public function testOffsetLimitStatement() $expected = 'SELECT id FROM book WHERE 1 LIMIT 50, 25'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testNegativeOffsetLimitStatement() @@ -160,7 +157,7 @@ public function testNegativeOffsetLimitStatement() $expected = 'SELECT id FROM book WHERE 1 LIMIT 25, 20'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testSelectDistinctStatement() @@ -170,15 +167,18 @@ public function testSelectDistinctStatement() $expected = 'SELECT DISTINCT(name) FROM book WHERE 1'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); + } + public function testSelectDistinctJoinStatement() + { $this->db->selectDistinct('author.name') ->from('book') ->innerJoin('author', 'author.id=book.author'); $expected = 'SELECT DISTINCT(author.name) FROM book INNER JOIN author ON author.id=book.author WHERE 1'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testOrderByStatement() @@ -192,7 +192,7 @@ public function testOrderByStatement() $expected = 'SELECT name FROM book WHERE 1 ORDER BY authorId ASC, id DESC LIMIT 10'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testInsertStatementBuild() @@ -210,12 +210,12 @@ public function testInsertStatementBuild() $expected = 'INSERT INTO post (title, content, author, status) VALUES (:title, :content, 5, :status);'; $this->assertEquals($expected, $this->db->generateInsertQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testInsertBatchStatementBuild() { - Parameters::reset(); + $this->db->from('post'); $this->db->set([ @@ -232,14 +232,14 @@ public function testInsertBatchStatementBuild() $expected = 'INSERT INTO post (title, content, author, status) VALUES (:title, :content, 5, :status), (:title_1, :content_1, NULL, :status_1);'; $this->assertEquals($expected, $this->db->generateBatchInsertQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testUpdateStatementBuild() { - Parameters::reset(); + $this->db->from('post') - ->where('status', true) + ->where('status', '=', true) ->limit(5); $data = [ @@ -251,14 +251,14 @@ public function testUpdateStatementBuild() $expected = 'UPDATE post SET title = :title, status = :status_1 WHERE status = :status LIMIT 5'; $this->assertEquals($expected, $this->db->generateUpdateQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testUpdateBatchStatementBuild() { - Parameters::reset(); + $this->db->from('post') - ->where('status', true); + ->where('status', '=', true); $this->db->set([ 'id' => 5, @@ -272,20 +272,20 @@ public function testUpdateBatchStatementBuild() $expected = 'UPDATE post SET title = CASE WHEN id = 5 THEN :title WHEN id = 10 THEN :title_1 ELSE title END, content = CASE WHEN id = 5 THEN :content ELSE content END WHERE status = :status AND id IN (5, 10)'; $this->assertEquals($expected, $this->db->generateUpdateBatchQuery('id')); - $this->db->reset(); + $this->db->resetStructure(); } public function testDeleteStatementBuild() { - Parameters::reset(); + $this->db->from('post') - ->where('authorId', 5) + ->where('authorId', '=', 5) ->limit(100); $expected = 'DELETE FROM post WHERE authorId = 5 LIMIT 100'; $this->assertEquals($expected, $this->db->generateDeleteQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testWhereSQLFunctionStatementBuild() @@ -296,7 +296,7 @@ public function testWhereSQLFunctionStatementBuild() $expected = 'SELECT * FROM post WHERE date BETWEEN :date AND CURDATE()'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testWhereRegexpSQLStatementBuild() @@ -307,13 +307,11 @@ public function testWhereRegexpSQLStatementBuild() $expected = 'SELECT * FROM post WHERE title REGEXP :title'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testSelectCoalesceSQLStatementBuild() { - - Parameters::reset(); $this->db->select('post.title') ->selectCoalesce('stat.view', 0) ->from('post') @@ -323,9 +321,11 @@ public function testSelectCoalesceSQLStatementBuild() $expected = 'SELECT post.title, COALESCE(stat.view, 0) FROM post LEFT JOIN stat ON stat.id=post.id WHERE post.id = 5'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); + } - Parameters::reset(); + public function testSelectCoalesceDefaultValue() + { $this->db->select('post.title') ->selectCoalesce('stat.view', 'post.view', 'views') ->from('post') @@ -335,14 +335,12 @@ public function testSelectCoalesceSQLStatementBuild() $expected = 'SELECT post.title, COALESCE(stat.view, post.view) AS views FROM post LEFT JOIN stat ON stat.id=post.id WHERE post.id = 5'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testTableAliasSQLStatementBuild() { - - Parameters::reset(); $this->db->select('p.title') ->select('s.view as s_view') ->from('post as p') @@ -352,9 +350,11 @@ public function testTableAliasSQLStatementBuild() $expected = 'SELECT p.title, s.view as s_view FROM post as p LEFT JOIN stat as s ON s.id=p.id WHERE p.id = 5'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); + } - Parameters::reset(); + public function testTableJoinAliasSQLStatementBuild() + { $this->db->select('p.title') ->select('s.view as s_view') ->from('post p') @@ -364,49 +364,61 @@ public function testTableAliasSQLStatementBuild() $expected = 'SELECT p.title, s.view as s_view FROM post p LEFT JOIN stat s ON s.id=p.id WHERE p.id = 5'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testWhereGroupStatement() { - Parameters::reset(); + $this->db->select('id') + ->from('users') + ->where('status', 1) + ->group(function (QueryBuilder $builder) { + $builder->where('type', 3) + ->where('type', 4); + }); + + $expected = 'SELECT id FROM users WHERE status = 1 AND (type = 3 AND type = 4)'; + + $this->assertEquals($expected, $this->db->generateSelectQuery()); + $this->db->resetStructure(); + } + + public function testWhereGroupMultipleStatement() + { $this->db->select('id, title, content, url') ->from('posts') ->where('status', 1) - ->group(function (Database $db) { + ->group(function (QueryBuilder $db) { $db->where('user_id', 1) - ->where('datetime', date("Y-m-d"), '>='); + ->where('datetime', '>=', date("Y-m-d")); }, 'or') - ->group(function (Database $db) { - $db->group(function (Database $db) { - $db->where('id', 1) - ->where('status', 0); + ->group(function (QueryBuilder $db) { + $db->group(function (QueryBuilder $db) { + $db->where('id', 2) + ->where('status', 3); }, 'or') - ->group(function (Database $db) { - $db->where('id', 2) - ->where('status', 1); + ->group(function (QueryBuilder $db) { + $db->where('id', 4) + ->where('status', 5); }, 'or'); }, 'or'); - - - $expected = 'SELECT id, title, content, url FROM posts WHERE status = 1 AND (user_id = 1 AND datetime >= :datetime) OR ((id = 1 AND status = 0) OR (id = 2 AND status = 1))'; + $expected = 'SELECT id, title, content, url FROM posts WHERE status = 1 AND (user_id = 1 AND datetime >= :datetime) OR ((id = 2 AND status = 3) OR (id = 4 AND status = 5))'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); - - Parameters::reset(); + $this->db->resetStructure(); } + public function testJoinClosureGive() { - Parameters::reset(); + $this->db->select('u.id', 'u.name', 'u.status, p.title') ->from('users AS u') ->where('u.status', 1) ->join('posts AS p', function (QueryBuilder $builder) { $builder->on('p.user_id', 'u.id') - ->where('p.publisher_time', $builder->raw('NOW()'), '>='); + ->where('p.publisher_time', '>=', $builder->raw('NOW()')); }) ->join('categories AS c', function (QueryBuilder $builder) { $builder->on('c.id', 'p.category_id') @@ -417,25 +429,26 @@ public function testJoinClosureGive() $expected = 'SELECT u.id, u.name, u.status, p.title FROM users AS u INNER JOIN posts AS p ON p.user_id = u.id INNER JOIN categories AS c ON c.id = p.category_id AND c.blog_id = u.blog_id WHERE u.status = 1 AND p.publisher_time >= NOW() AND c.status = 1 HAVING COUNT(p.category_id) > 1 LIMIT 5'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } public function testSubQuery() { - Parameters::reset(); + $this->db->select('u.name') ->from('users AS u') - ->in('u.id', $this->db->subQuery(function (QueryBuilder $builder) { + ->whereIn('u.id', $this->db->subQuery(function (QueryBuilder $builder) { $builder->select('id') ->from('roles') ->where('name', 'admin'); })); $expected = 'SELECT u.name FROM users AS u WHERE u.id IN (SELECT id FROM roles WHERE name = :name)'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); - + $this->db->resetStructure(); + } - Parameters::reset(); + public function testSubQueryJoinTable() + { $this->db->select('u.name, p.title') ->from('users AS u') ->join($this->db->subQuery(function (QueryBuilder $builder) { @@ -446,7 +459,7 @@ public function testSubQuery() $expected = 'SELECT u.name, p.title FROM users AS u JOIN (SELECT id, title, user_id FROM posts WHERE user_id = 5) AS p ON p.user_id = u.id WHERE 1'; $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->reset(); + $this->db->resetStructure(); } } From 3c6eb3813f0472bbb2c11b8971763f4dc6f890f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammet=20=C5=9EAFAK?= Date: Sat, 9 Dec 2023 23:45:58 +0300 Subject: [PATCH 2/2] InitORM --- README.md | 69 +- composer.json | 3 +- src/Connection/Connection.php | 146 -- .../Exceptions/ConnectionException.php | 8 - .../Interfaces/ConnectionInterface.php | 60 - src/DB.php | 52 + src/DBAL/CRUD.php | 165 -- src/DBAL/Database.php | 292 ---- .../Exceptions/SQLQueryExecuteException.php | 9 - src/DBAL/Interfaces/CRUDInterface.php | 82 - src/DBAL/Interfaces/DatabaseInterface.php | 99 -- src/DBAL/Interfaces/ResultInterface.php | 82 - src/DBAL/Result.php | 144 -- src/Database.php | 8 + src/Entity.php | 8 + src/Facade/DB.php | 189 --- src/Model.php | 8 + src/ORM/Entity.php | 128 -- src/ORM/Exceptions/DeletableException.php | 7 - src/ORM/Exceptions/EntityNotMethod.php | 14 - src/ORM/Exceptions/ModelException.php | 10 - src/ORM/Exceptions/ReadableException.php | 7 - src/ORM/Exceptions/UpdatableException.php | 7 - src/ORM/Exceptions/WritableException.php | 7 - src/ORM/Interfaces/EntityInterface.php | 18 - src/ORM/Interfaces/ModelInterface.php | 107 -- src/ORM/Model.php | 248 --- .../Exceptions/QueryBuilderException.php | 8 - .../Exceptions/QueryGeneratorException.php | 9 - .../Interfaces/ParameterInterface.php | 48 - .../Interfaces/QueryBuilderInterface.php | 758 --------- src/QueryBuilder/Parameters.php | 104 -- src/QueryBuilder/QueryBuilder.php | 1455 ----------------- src/QueryBuilder/RawQuery.php | 53 - src/Utils/Datatables.php | 30 +- src/Utils/Helper.php | 37 - tests/QueryBuilderUnitTest.php | 465 ------ 37 files changed, 120 insertions(+), 4824 deletions(-) delete mode 100644 src/Connection/Connection.php delete mode 100644 src/Connection/Exceptions/ConnectionException.php delete mode 100644 src/Connection/Interfaces/ConnectionInterface.php create mode 100644 src/DB.php delete mode 100644 src/DBAL/CRUD.php delete mode 100644 src/DBAL/Database.php delete mode 100644 src/DBAL/Exceptions/SQLQueryExecuteException.php delete mode 100644 src/DBAL/Interfaces/CRUDInterface.php delete mode 100644 src/DBAL/Interfaces/DatabaseInterface.php delete mode 100644 src/DBAL/Interfaces/ResultInterface.php delete mode 100644 src/DBAL/Result.php create mode 100644 src/Database.php create mode 100644 src/Entity.php delete mode 100644 src/Facade/DB.php create mode 100644 src/Model.php delete mode 100644 src/ORM/Entity.php delete mode 100644 src/ORM/Exceptions/DeletableException.php delete mode 100644 src/ORM/Exceptions/EntityNotMethod.php delete mode 100644 src/ORM/Exceptions/ModelException.php delete mode 100644 src/ORM/Exceptions/ReadableException.php delete mode 100644 src/ORM/Exceptions/UpdatableException.php delete mode 100644 src/ORM/Exceptions/WritableException.php delete mode 100644 src/ORM/Interfaces/EntityInterface.php delete mode 100644 src/ORM/Interfaces/ModelInterface.php delete mode 100644 src/ORM/Model.php delete mode 100644 src/QueryBuilder/Exceptions/QueryBuilderException.php delete mode 100644 src/QueryBuilder/Exceptions/QueryGeneratorException.php delete mode 100644 src/QueryBuilder/Interfaces/ParameterInterface.php delete mode 100644 src/QueryBuilder/Interfaces/QueryBuilderInterface.php delete mode 100644 src/QueryBuilder/Parameters.php delete mode 100644 src/QueryBuilder/QueryBuilder.php delete mode 100644 src/QueryBuilder/RawQuery.php delete mode 100644 src/Utils/Helper.php delete mode 100644 tests/QueryBuilderUnitTest.php diff --git a/README.md b/README.md index 98f6c8c..6415f09 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ composer require initphp/database ```php require_once "vendor/autoload.php"; -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; // Connection DB::createImmutable([ @@ -41,7 +41,7 @@ DB::createImmutable([ #### Create ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; $data = [ 'title' => 'Post Title', 'content' => 'Post Content', @@ -59,17 +59,13 @@ $isInsert = DB::create('post', $data); */ if($isInsert){ // Success -} else { - foreach (DB::getErrors() as $errMsg) { - echo $errMsg; - } } ``` ##### Create Batch ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; $data = [ [ @@ -97,17 +93,13 @@ $isInsert = DB::createBatch('post', $data); if($isInsert){ // Success -} else { - foreach (DB::getErrors() as $errMsg) { - echo $errMsg; - } } ``` #### Read ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; /** @@ -120,6 +112,7 @@ use \InitPHP\Database\Facade\DB; * LIMIT 20, 10 */ +/** @var \InitORM\DBAL\DataMapper\Interfaces\DataMapperInterface $res */ $res = DB::select('user.name as author_name', 'post.id', 'post.title') ->from('post') ->selfJoin('user', 'user.id=post.author') @@ -127,7 +120,7 @@ $res = DB::select('user.name as author_name', 'post.id', 'post.title') ->orderBy('post.id', 'ASC') ->orderBy('post.created_at', 'DESC') ->offset(20)->limit(10) - ->read('post'); + ->read(); if($res->numRows() > 0){ $results = $res->asAssoc() @@ -141,7 +134,7 @@ if($res->numRows() > 0){ #### Update ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; $data = [ 'title' => 'New Title', 'content' => 'New Content', @@ -159,17 +152,13 @@ $isUpdate = DB::where('id', 13) */ if ($isUpdate) { // Success -} else { - foreach (DB::getErrors() as $errMsg) { - echo $errMsg; - } } ``` ##### Update Batch ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; $data = [ [ 'id' => 5, @@ -183,7 +172,7 @@ $data = [ ]; $isUpdate = DB::where('status', '!=', 0) - ->updateBatch('post', $data, 'id'); + ->updateBatch('id', 'post', $data); /** * This executes the following query. @@ -200,17 +189,13 @@ $isUpdate = DB::where('status', '!=', 0) */ if ($isUpdate) { // Success -} else { - foreach (DB::getErrors() as $errMsg) { - echo $errMsg; - } } ``` #### Delete ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; $isDelete = DB::where('id', 13) ->delete('post'); @@ -222,18 +207,15 @@ $isDelete = DB::where('id', 13) */ if ($isUpdate) { // Success -} else { - foreach (DB::getErrors() as $errMsg) { - echo $errMsg; - } } ``` ### RAW ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; +/** @var \InitORM\DBAL\DataMapper\Interfaces\DataMapperInterface $res */ $res = DB::query("SELECT id FROM post WHERE user_id = :id", [ ':id' => 5 ]); @@ -242,12 +224,13 @@ $res = DB::query("SELECT id FROM post WHERE user_id = :id", [ #### Builder for RAW ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; +/** @var \InitORM\DBAL\DataMapper\Interfaces\DataMapperInterface $res */ $res = DB::select(DB::raw("CONCAT(name, ' ', surname) AS fullname")) ->where(DB::raw("title = '' AND (status = 1 OR status = 0)")) ->limit(5) - ->get('users'); + ->read('users'); /** * SELECT CONCAT(name, ' ', surname) AS fullname @@ -269,7 +252,7 @@ This library was developed with the thought that you would work with a single da If you want to work with a different non-global connection, use the `connect()` method. ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; DB::connect([ 'dsn' => 'mysql:host=localhost;port=3306;dbname=test;charset=utf8mb4', @@ -310,7 +293,7 @@ class Posts extends \InitPHP\Database\Model /** * If not specified, \InitPHP\Database\Entity::class is used by default. * - * @var \InitPHP\Database\Orm\Entity|string + * @var string<\InitPHP\Database\Entity> */ protected $entity = \App\Entities\PostEntity::class; @@ -324,9 +307,9 @@ class Posts extends \InitPHP\Database\Model /** * The name of the PRIMARY KEY column. If not, define it as NULL. * - * @var null|string + * @var string */ - protected ?string $schemaId = 'id'; + protected string $schemaId = 'id'; /** * Specify FALSE if you want the data to be permanently deleted. @@ -372,7 +355,7 @@ The most basic example of a entity class would look like this. ```php namespace App\Entities; -class PostEntity extends \InitPHP\Database\ORM\Entity +class PostEntity extends \InitPHP\Database\Entity { /** * An example of a getter method for the "post_title" column. @@ -406,7 +389,7 @@ Below I have mentioned some developer tools that you can use during and after de ### Logger ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; DB::createImmutable([ 'dsn' => 'mysql:host=localhost;dbname=test;port=3306;charset=utf8mb4;', @@ -424,7 +407,7 @@ _Note :_ You can define variables such as `{year}`, `{month}`, `{day}` in the fi - You can also define an object with the `critical` method. The database library will pass the log message to this method as a parameter. Or define it as callable array to use any method of the object. ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; class Logger { @@ -450,7 +433,7 @@ DB::createImmutable([ - Similarly it is possible to define it in a callable method. ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; DB::createImmutable([ 'dsn' => 'mysql:host=localhost;dbname=test;port=3306;charset=utf8mb4;', @@ -469,7 +452,7 @@ DB::createImmutable([ Debug mode is used to include the executed SQL statement in the error message. *__It should only be activated in the development environment__*. ```php -use \InitPHP\Database\Facade\DB; +use \InitPHP\Database\DB; DB::createImmutable([ 'dsn' => 'mysql:host=localhost;dbname=test;port=3306;charset=utf8mb4;', @@ -485,11 +468,11 @@ DB::createImmutable([ Profiler mode is a developer tool available in v3 and above. It is a feature that allows you to see the executed queries along with their execution times. ```php -use InitPHP\Database\Facade\DB; +use InitPHP\Database\DB; DB::enableQueryLog(); -DB::table('users')->where('name', 'John')->get(); +DB::table('users')->where('name', 'John')->read(); var_dump(DB::getQueryLogs()); diff --git a/composer.json b/composer.json index ffb1fd3..98d8e7e 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "minimum-stability": "stable", "require": { "php": ">=8.0", - "ext-pdo": "*" + "ext-pdo": "*", + "initorm/orm": "^1.0" }, "require-dev": { "phpunit/phpunit": "^10.4" diff --git a/src/Connection/Connection.php b/src/Connection/Connection.php deleted file mode 100644 index 233b8e1..0000000 --- a/src/Connection/Connection.php +++ /dev/null @@ -1,146 +0,0 @@ - '', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - - 'debug' => false, - 'log' => null, - ]; - - private ?PDO $pdo = null; - - private array $transaction = [ - 'status' => false, - 'enable' => false, - 'testMode' => false, - ]; - - public function __construct(?array $credentials = null) - { - !empty($credentials) && $this->credentials = array_merge($this->credentials, $credentials); - } - - /** - * @inheritDoc - */ - public function getCredentials(?string $key = null, mixed $default = null): mixed - { - if ($key === null) { - return $this->credentials; - } - - return $this->credentials[$key] ?? $default; - } - - /** - * @inheritDoc - */ - public function getPDO(): PDO - { - !isset($this->pdo) && $this->connect(); - - return $this->pdo; - } - - /** - * @inheritDoc - */ - public function connect(): bool - { - try { - $options = [ - PDO::ATTR_EMULATE_PREPARES => false, - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_CLASS, - ]; - - $this->pdo = new PDO($this->getCredentials('dsn'), $this->getCredentials('username'), $this->getCredentials('password'), $options); - - if ($charset = $this->getCredentials('charset')) { - if ($collation = $this->getCredentials('collation')) { - $this->pdo->exec("SET NAMES '" . $charset . "' COLLATE '" . $collation . "'"); - } - $this->pdo->exec("SET CHARACTER SET '" . $charset . "'"); - } - - return true; - } catch (Exception $e) { - throw new ConnectionException($e->getMessage(), (int)$e->getCode(), $e->getPrevious()); - } - } - - /** - * @inheritDoc - */ - public function beginTransaction(bool $testMode = false): bool - { - $this->transaction = [ - 'status' => true, - 'enable' => true, - 'testMode' => $testMode, - ]; - - return $this->getPDO()->beginTransaction(); - } - - /** - * @inheritDoc - */ - public function completeTransaction(): bool - { - return $this->transaction['status'] === false || $this->transaction['testMode'] === true ? $this->rollBack() : $this->commit(); - } - - /** - * @inheritDoc - */ - public function commit(): bool - { - $this->transaction = [ - 'status' => false, - 'enable' => false, - 'testMode' => false, - ]; - - return $this->getPDO()->commit(); - } - - /** - * @inheritDoc - */ - public function rollBack(): bool - { - $this->transaction = [ - 'status' => false, - 'enable' => false, - 'testMode' => false, - ]; - - return $this->getPDO()->rollBack(); - } - - /** - * @inheritDoc - */ - public function disconnect(): bool - { - $this->pdo = null; - - return true; - } - -} diff --git a/src/Connection/Exceptions/ConnectionException.php b/src/Connection/Exceptions/ConnectionException.php deleted file mode 100644 index 226baf2..0000000 --- a/src/Connection/Exceptions/ConnectionException.php +++ /dev/null @@ -1,8 +0,0 @@ -{$name}(...$arguments); + } + + public static function __callStatic($name, $arguments) + { + return self::getDatabase()->{$name}(...$arguments); + } + + public static function createImmutable(array|ConnectionInterface $connection): void + { + self::$db = self::connect($connection); + } + + /** + * @param array|ConnectionInterface $connection + * @return DatabaseInterface + */ + public static function connect(array|ConnectionInterface $connection): DatabaseInterface + { + return new Database($connection); + } + + public static function getDatabase(): DatabaseInterface + { + if (!isset(self::$db)) { + throw new DatabaseException('To create an immutable, first use the "createImmutable()" method.'); + } + + return self::$db; + } + + +} diff --git a/src/DBAL/CRUD.php b/src/DBAL/CRUD.php deleted file mode 100644 index 92fcc17..0000000 --- a/src/DBAL/CRUD.php +++ /dev/null @@ -1,165 +0,0 @@ -db = &$db; - } - - public function __call(string $name, array $arguments) - { - $res = $this->db->{$name}(...$arguments); - - return ($res instanceof DatabaseInterface) ? $this : $res; - } - - /** - * @inheritDoc - */ - public function create(?string $table = null, ?array $set = null): bool - { - $builder = $this->db->getQueryBuilder(); - - !empty($set) && $builder->set($set); - - !empty($table) && $builder->from($table); - - $res = $this->db->query($builder->generateInsertQuery()); - $this->db->getQueryBuilder()->getParameter()->reset(); - - return $res->numRows() > 0; - } - - /** - * @inheritDoc - */ - public function createBatch(?string $table = null, ?array $set = null): bool - { - $builder = $this->db->getQueryBuilder(); - - if (!empty($set)) { - foreach ($set as $row) { - !empty($row) && is_array($row) - && $builder->set($row); - } - } - - !empty($table) && $builder->from($table); - - $res = $this->db->query($builder->generateBatchInsertQuery()); - $this->db->getQueryBuilder()->resetStructure(); - - return $res->numRows() > 0; - } - - /** - * @inheritDoc - */ - public function read(?string $table = null, array $selector = [], array $conditions = [], array $parameters = []): ResultInterface - { - $builder = $this->db->getQueryBuilder(); - - !empty($parameters) && $builder->getParameter()->merge($parameters); - - !empty($table) && $builder->from($table); - - - $res = $this->db->query($builder->generateSelectQuery($selector, $conditions)); - $this->db->getQueryBuilder()->resetStructure(); - - return $res; - } - - /** - * @inheritDoc - */ - public function readOne(?string $table = null, array $selector = [], array $conditions = [], array $parameters = []): ResultInterface - { - $builder = $this->db->getQueryBuilder(); - - !empty($parameters) && $builder->getParameter()->merge($parameters); - - !empty($table) && $builder->from($table); - - $res = $this->db->query($builder->limit(1) - ->generateSelectQuery($selector, $conditions)); - - $this->db->getQueryBuilder()->resetStructure(); - - return $res; - } - - /** - * @inheritDoc - */ - public function update(?string $table = null, ?array $set = null): bool - { - $builder = $this->db->getQueryBuilder(); - - !empty($set) && $builder->set($set); - !empty($table) && $builder->from($table); - - $res = $this->db->query($builder - ->generateUpdateQuery()); - - $this->db->getQueryBuilder()->resetStructure(); - - return $res->numRows() > 0; - } - - /** - * @inheritDoc - */ - public function updateBatch(?string $table = null, ?array $set = null, ?string $referenceColumn = null): bool - { - $builder = $this->db->getQueryBuilder(); - - if (!empty($set)) { - foreach ($set as $row) { - if (!empty($row) && is_array($row)) { - $builder->set($row); - } - } - } - !empty($table) && $builder->from($table); - - $res = $this->db->query($builder - ->generateUpdateBatchQuery($referenceColumn)); - - $this->db->getQueryBuilder()->resetStructure(); - - return $res->numRows() > 0; - } - - /** - * @inheritDoc - */ - public function delete(?string $table = null, ?array $conditions = []): bool - { - $builder = $this->db->getQueryBuilder(); - - if (!empty($conditions)) { - foreach ($conditions as $column => $value) { - if (is_string($column)) { - $builder->where($column, $value); - } else { - $builder->where($value); - } - } - } - !empty($table) && $builder->from($table); - - $res = $this->db->query($builder->generateDeleteQuery()); - - return $res->numRows() > 0; - } - -} diff --git a/src/DBAL/Database.php b/src/DBAL/Database.php deleted file mode 100644 index 22b6b5c..0000000 --- a/src/DBAL/Database.php +++ /dev/null @@ -1,292 +0,0 @@ - true, - 'queryLog' => false, - ]; - - private ResultInterface $lastResult; - - private array $queryLogs = []; - - private array $errors = []; - - public function __construct(ConnectionInterface $connection, QueryBuilderInterface $builder) - { - $this->connection = $connection; - $this->builder = $builder; - $this->queryOptions = array_merge($this->queryOptions, $connection->getCredentials()); - } - - /** - * @throws Exception - */ - public function __call(string $name, array $arguments) - { - if (method_exists($this->connection, $name)) { - $res = $this->connection->{$name}(...$arguments); - - return ($res instanceof ConnectionInterface) ? $this : $res; - } - - if (method_exists($this->builder, $name)) { - $res = $this->connection->{$name}(...$arguments); - - return ($res instanceof QueryBuilderInterface) ? $this : $res; - } - - throw new Exception("There is no method called \"" . $name . "\""); - } - - /** - * @inheritDoc - */ - public function getErrors(): array - { - return $this->errors ?? []; - } - - /** - * @inheritDoc - */ - public function getQueryBuilder(): QueryBuilderInterface - { - return $this->builder; - } - - /** - * @inheritDoc - */ - public function getConnection(): ConnectionInterface - { - return $this->connection; - } - - /** - * @inheritDoc - */ - public function builder(): QueryBuilderInterface - { - return $this->getQueryBuilder()->newBuilder(); - } - - /** - * @inheritDoc - */ - public function newInstance(ConnectionInterface|array $connectionOrCredentials, ?QueryBuilderInterface $builder = null): self - { - return new self(($connectionOrCredentials instanceof ConnectionInterface) ? $connectionOrCredentials : new Connection($connectionOrCredentials), $builder ?? $this->builder); - } - - /** - * @inheritDoc - */ - public function get(?string $table = null, ?array $selection = null, ?array $conditions = null): ResultInterface - { - !empty($table) && $this->getQueryBuilder()->addFrom($table); - $res = $this->query($this->getQueryBuilder()->generateSelectQuery($selection ?? [], $conditions ?? [])); - $this->getQueryBuilder() - ->resetStructure(); - - return $res; - } - - /** - * @inheritDoc - */ - public function query(string $rawSQL, ?array $arguments = null, ?array $options = null): ResultInterface - { - $arguments = array_merge($this->getQueryBuilder()->getParameter()->all(), ($arguments ?? [])); - $options = array_merge($this->queryOptions, ($options ?? [])); - - try { - $timerStart = $options['profiler'] ? microtime(true) : 0; - $stmt = $this->getConnection()->getPDO()->prepare($rawSQL); - if ($stmt === false) { - throw new SQLQueryExecuteException('The SQL query could not be prepared.'); - } - if (!empty($arguments)) { - foreach ($arguments as $key => $value) { - $stmt->bindValue($key, $value, $this->__getValueBindType($value)); - } - } - if ($options['parameterReset']) { - $this->getQueryBuilder() - ->getParameter() - ->reset(); - } - $execute = $stmt->execute(); - if ($options['queryLog']) { - $timer = round((microtime(true) - $timerStart), 5); - $this->queryLogs[] = [ - 'query' => $rawSQL, - 'time' => $timer, - 'args' => $arguments, - ]; - } - if ($execute === false) { - throw new SQLQueryExecuteException('The SQL query could not be executed.'); - } - $errorCode = $stmt->errorCode(); - if($errorCode !== null && !empty(trim($errorCode, "0 \t\n\r\0\x0B"))){ - $errorInfo = $stmt->errorInfo(); - if(isset($errorInfo[2])){ - $msg = $errorCode . ' - ' . $errorInfo[2]; - $this->errors[] = $msg; - !empty($options['log']) && $this->createLog(($msg . ' SQL : ' . $rawSQL), $options['log']); - } - } - $this->lastResult = new Result($stmt); - - return !empty($options['fetch_mode']) - ? $this->lastResult->setFetchMode($options['fetch_mode']) - : $this->lastResult; - - } catch (Exception $e) { - $message = $e->getMessage(); - $sqlQuery = empty($arguments) ? $rawSQL : strtr($rawSQL, $arguments); - !empty($options['log']) && $this->createLog(($message . ' SQL : ' . $sqlQuery), $options['log']); - if ($options['debug'] === true) { - $message .= ' SQL : ' . $sqlQuery; - } - if ($options['parameterReset']) { - $this->getQueryBuilder() - ->getParameter() - ->reset(); - } - throw new SQLQueryExecuteException($message, (int)$e->getCode()); - } - } - - /** - * @inheritDoc - */ - public function count(): int - { - $builder = $this->getQueryBuilder()->clone() - ->resetStructure('select', false); - - $builder->selectCount('*', 'row_count'); - - $res = $this->query($builder->generateSelectQuery(), null, [ - 'parameterReset' => false, - ]); - - return $res->asAssoc()->row()['row_count'] ?? 0; - } - - /** - * @inheritDoc - */ - public function insertId(): int - { - $id = $this->getConnection()->getPDO()->lastInsertId(); - - return $id === false ? 0 : (int)$id; - } - - - /** - * @inheritDoc - */ - public function enableQueryLog(): DatabaseInterface - { - $this->queryOptions['queryLog'] = true; - - return $this; - } - - /** - * @inheritDoc - */ - public function disableQueryLog(): DatabaseInterface - { - $this->queryOptions['queryLog'] = false; - - return $this; - } - - /** - * @inheritDoc - */ - public function getQueryLogs(): array - { - return $this->queryLogs; - } - - /** - * @inheritDoc - */ - public function transaction(Closure $closure, int $attempt = 1, bool $testMode = false): bool - { - $attempt < 1 && $attempt = 1; - $res = false; - for ($i = 0; $i < $attempt; ++$i) { - try { - $this->getConnection()->beginTransaction($testMode); - call_user_func_array($closure, [$this]); - $res = $testMode ? $this->getConnection()->rollBack() : $this->getConnection()->commit(); - break; - } catch (Throwable $e) { - $res = $this->getConnection()->rollBack(); - $this->createLog('Transaction Rollback : ' . $e->getMessage()); - } - } - - return $res; - } - - protected function __getValueBindType($value): int - { - return match (true) { - is_bool($value), is_int($value) => PDO::PARAM_INT, - is_null($value) => PDO::PARAM_NULL, - default => PDO::PARAM_STR, - }; - } - - private function createLog(string $message, mixed $handler = null): void - { - $handler === null && $handler = $this->getConnection()->getCredentials('log'); - if (empty($handler)) { - return; - } - if (is_callable($handler)) { - call_user_func_array($handler, [$message]); - } else if (is_object($handler) && method_exists($handler, 'critical')) { - $handler->critical($message); - } else if (is_string($handler)) { - $path = strtr($handler, [ - '{timestamp}' => time(), - '{date}' => date("Y-m-d"), - '{year}' => date("Y"), - '{month}' => date("m"), - '{day}' => date("d"), - '{hour}' => date("H"), - '{minute}' => date("i"), - '{second}' => date("s"), - ]); - @file_put_contents($path, $message, FILE_APPEND); - } - - } - -} diff --git a/src/DBAL/Exceptions/SQLQueryExecuteException.php b/src/DBAL/Exceptions/SQLQueryExecuteException.php deleted file mode 100644 index c80352a..0000000 --- a/src/DBAL/Exceptions/SQLQueryExecuteException.php +++ /dev/null @@ -1,9 +0,0 @@ -\PDO::FETCH_*

- * @return self - */ - public function setFetchMode(int $mode): self; - - /** - * @param string $class - * @return self - */ - public function asClass(string $class = Entity::class): self; - - /** - * @return self - */ - public function asObject(): self; - - - /** - * @return self - */ - public function asAssoc(): self; - - /** - * @return self - */ - public function asArray(): self; - - /** - * @return self - */ - public function asLazy(): self; - - /** - * @return array|object|false - */ - public function row(): array|object|false; - - /** - * @return array[]|object[]|false - */ - public function rows(): array|false; - - -} \ No newline at end of file diff --git a/src/DBAL/Result.php b/src/DBAL/Result.php deleted file mode 100644 index cf2febe..0000000 --- a/src/DBAL/Result.php +++ /dev/null @@ -1,144 +0,0 @@ -setStatement($statement); - } - - /** - * @inheritDoc - */ - public function setStatement(PDOStatement $statement): self - { - $this->statement = $statement; - $this->query = $statement->queryString; - $this->numRows = $statement->rowCount(); - - return $this; - } - - /** - * @inheritDoc - */ - public function withStatement(PDOStatement $statement): self - { - return new self($statement); - } - - /** - * @inheritDoc - */ - public function getStatement(): PDOStatement - { - return $this->statement; - } - - /** - * @inheritDoc - */ - public function numRows(): int - { - return $this->numRows; - } - - /** - * @inheritDoc - */ - public function query(): string - { - return $this->query; - } - - /** - * @inheritDoc - */ - public function setFetchMode(int $mode): self - { - $this->getStatement()->setFetchMode($mode); - - return $this; - } - - /** - * @inheritDoc - */ - public function asClass(string $class = Entity::class): self - { - $this->getStatement()->setFetchMode(PDO::FETCH_CLASS, $class); - - return $this; - } - - /** - * @inheritDoc - */ - public function asObject(): self - { - $this->getStatement()->setFetchMode(PDO::FETCH_OBJ); - - return $this; - } - - /** - * @inheritDoc - */ - public function asAssoc(): self - { - $this->getStatement()->setFetchMode(PDO::FETCH_ASSOC); - - return $this; - } - - /** - * @inheritDoc - */ - public function asArray(): self - { - $this->getStatement()->setFetchMode(PDO::FETCH_BOTH); - - return $this; - } - - /** - * @inheritDoc - */ - public function asLazy(): self - { - $this->getStatement()->setFetchMode(PDO::FETCH_LAZY); - - return $this; - } - - /** - * @inheritDoc - */ - public function row(): array|object|false - { - return $this->getStatement()->fetch(); - } - - /** - * @inheritDoc - */ - public function rows(): array|false - { - return $this->getStatement()->fetchAll(); - } - -} diff --git a/src/Database.php b/src/Database.php new file mode 100644 index 0000000..e866994 --- /dev/null +++ b/src/Database.php @@ -0,0 +1,8 @@ + - * @copyright Copyright © 2022 Muhammet ŞAFAK - * @license ./LICENSE MIT - * @version 3.0 - * @link https://www.muhammetsafak.com.tr - */ - -namespace InitPHP\Database\Facade; - -use \InitPHP\Database\Connection\{Connection, - Interfaces\ConnectionInterface}; -use \InitPHP\Database\DBAL\{CRUD, - Database, - Interfaces\CRUDInterface, - Interfaces\ResultInterface}; -use \InitPHP\Database\QueryBuilder\{QueryBuilder, - RawQuery, - Interfaces\ParameterInterface, - Interfaces\QueryBuilderInterface}; -use PDO; -use Closure; - -/** - * @mixin CRUDInterface - * @method static mixed getCredentials(?string $key = null, mixed $default = null) - * @method static PDO getPDO() - * @method static bool beginTransaction(bool $testMode = false) - * @method static bool completeTransaction() - * @method static bool commit() - * @method static bool rollBack() - * @method static bool disconnect() - * @method static CRUDInterface enableQueryLog() - * @method static CRUDInterface disableQueryLog() - * @method static CRUDInterface getQueryLogs() - * @method static QueryBuilderInterface getQueryBuilder() - * @method static ConnectionInterface getConnection() - * @method static QueryBuilderInterface builder() - * @method static CRUDInterface newInstance(ConnectionInterface|array $connectionOrCredentials, ?QueryBuilderInterface $builder = null) - * @method static ResultInterface get(?string $table = null, ?array $selection = null, ?array $conditions = null) - * @method static ResultInterface query(string $rawSQL, ?array $arguments = null, ?array $options = null) - * @method static int count() - * @method static int insertId() - * @method static bool transaction(Closure $closure, int $attempt = 1, bool $testMode = false) - * @method static bool create(?string $table = null, ?array $set = null) - * @method static bool createBatch(?string $table = null, ?array $set = null) - * @method static ResultInterface read(?string $table = null, array $selector = [], array $conditions = [], array $parameters = []) - * @method static ResultInterface readOne(?string $table = null, array $selector = [], array $conditions = [], array $parameters = []) - * @method static bool update(?string $table = null, ?array $set = null) - * @method static bool updateBatch(?string $table = null, ?array $set = null, ?string $referenceColumn = null) - * @method static bool delete(?string $table = null, ?array $conditions = []) - * @method static CRUDInterface newBuilder() - * @method static CRUDInterface importQB(array $structure, bool $merge = false) - * @method static array exportQB() - * @method static ParameterInterface getParameter() - * @method static CRUDInterface setParameter(string $key, mixed $value) - * @method static CRUDInterface setParameters(array $parameters = []) - * @method static CRUDInterface select(string|RawQuery|string[]|RawQuery[] ...$columns) - * @method static CRUDInterface selectCount(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectCountDistinct(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectMax(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectMin(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectAvg(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectAs(RawQuery|string $column, string $alias) - * @method static CRUDInterface selectUpper(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectLower(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectLength(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectMid(RawQuery|string $column, int $offset, int $length, ?string $alias = null) - * @method static CRUDInterface selectLeft(RawQuery|string $column, int $length, ?string $alias = null) - * @method static CRUDInterface selectRight(RawQuery|string $column, int $length, ?string $alias = null) - * @method static CRUDInterface selectDistinct(RawQuery|string $column, ?string $alias = null) - * @method static CRUDInterface selectCoalesce(RawQuery|string $column, mixed $default = '0', ?string $alias = null) - * @method static CRUDInterface selectSum(string|RawQuery $column, ?string $alias = null) - * @method static CRUDInterface selectConcat(array $columns, ?string $alias = null) - * @method static CRUDInterface from(RawQuery|string $table, ?string $alias = null) - * @method static CRUDInterface addFrom(RawQuery|string $table, ?string $alias = null) - * @method static CRUDInterface table(string|RawQuery $table) - * @method static CRUDInterface groupBy(string|RawQuery|array ...$columns) - * @method static CRUDInterface join(RawQuery|string $table, RawQuery|string|Closure $onStmt = null, string $type = 'INNER') - * @method static CRUDInterface selfJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) - * @method static CRUDInterface innerJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) - * @method static CRUDInterface leftJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) - * @method static CRUDInterface rightJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) - * @method static CRUDInterface leftOuterJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) - * @method static CRUDInterface rightOuterJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) - * @method static CRUDInterface naturalJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt) - * @method static CRUDInterface orderBy(RawQuery|string $column, string $soft = 'ASC') - * @method static CRUDInterface where(RawQuery|string $column, string $operator = '=', mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface having(RawQuery|string $column, string $operator = '=', mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface on(RawQuery|string $column, string $operator = '=', mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface set(RawQuery|array|string $column, mixed $value = null, bool $strict = true) - * @method static CRUDInterface addSet(RawQuery|array|string $column, mixed $value = null, bool $strict = true) - * @method static CRUDInterface andWhere(string|RawQuery $column, string $operator = '=', mixed $value = null) - * @method static CRUDInterface orWhere(string|RawQuery $column, string $operator = '=', mixed $value = null) - * @method static CRUDInterface between(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null, string $logical = 'AND') - * @method static CRUDInterface orBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null) - * @method static CRUDInterface andBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null) - * @method static CRUDInterface notBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null, string $logical = 'AND') - * @method static CRUDInterface orNotBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null) - * @method static CRUDInterface andNotBetween(string|RawQuery $column, mixed $firstValue = null, mixed $lastValue = null) - * @method static CRUDInterface findInSet(string|RawQuery $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface andFindInSet(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface orFindInSet(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface notFindInSet(string|RawQuery $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface andNotFindInSet(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface orNotFindInSet(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface whereIn(string|RawQuery $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface whereNotIn(string|RawQuery $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface orWhereIn(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface orWhereNotIn(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface andWhereIn(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface andWhereNotIn(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface regexp(string|RawQuery $column, string|RawQuery $value, string $logical = 'AND') - * @method static CRUDInterface andRegexp(string|RawQuery $column, string|RawQuery $value) - * @method static CRUDInterface orRegexp(string|RawQuery $column, string|RawQuery $value) - * @method static CRUDInterface soundex(string|RawQuery $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface andSoundex(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface orSoundex(string|RawQuery $column, mixed $value = null) - * @method static CRUDInterface whereIsNull(string|RawQuery $column, string $logical = 'AND') - * @method static CRUDInterface orWhereIsNull(string|RawQuery $column) - * @method static CRUDInterface andWhereIsNull(string|RawQuery $column) - * @method static CRUDInterface whereIsNotNull(string|RawQuery $column, string $logical = 'AND') - * @method static CRUDInterface orWhereIsNotNull(string|RawQuery $column) - * @method static CRUDInterface andWhereIsNotNull(string|RawQuery $column) - * @method static CRUDInterface offset(int $offset = 0) - * @method static CRUDInterface limit(int $limit) - * @method static CRUDInterface like(string|RawQuery|array $column, mixed $value = null, string $type = 'both', string $logical = 'AND') - * @method static CRUDInterface orLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both') - * @method static CRUDInterface andLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both') - * @method static CRUDInterface notLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both', string $logical = 'AND') - * @method static CRUDInterface orNotLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both') - * @method static CRUDInterface andNotLike(string|RawQuery|array $column, mixed $value = null, string $type = 'both') - * @method static CRUDInterface startLike(string|RawQuery|array $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface orStartLike(string|RawQuery|array $column, mixed $value = null) - * @method static CRUDInterface andStartLike(string|RawQuery|array $column, mixed $value = null) - * @method static CRUDInterface notStartLike(string|RawQuery|array $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface orStartNotLike(string|RawQuery|array $column, mixed $value = null) - * @method static CRUDInterface andStartNotLike(string|RawQuery|array $column, mixed $value = null) - * @method static CRUDInterface endLike(string|RawQuery|array $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface orEndLike(string|RawQuery|array $column, mixed $value = null) - * @method static CRUDInterface andEndLike(string|RawQuery|array $column, mixed $value = null) - * @method static CRUDInterface notEndLike(string|RawQuery|array $column, mixed $value = null, string $logical = 'AND') - * @method static CRUDInterface orEndNotLike(string|RawQuery|array $column, mixed $value = null) - * @method static CRUDInterface andEndNotLike(string|RawQuery|array $column, mixed $value = null) - * @method static RawQuery subQuery(Closure $closure, ?string $alias = null, bool $isIntervalQuery = true) - * @method static CRUDInterface group(Closure $closure) - * @method static RawQuery raw(mixed $rawQuery) - * @method static string[] getErrors() - */ -class DB -{ - - private static CRUDInterface $databaseInstance; - - public function __call($name, $arguments) - { - return self::getDatabase()->{$name}(...$arguments); - } - - public static function __callStatic($name, $arguments) - { - return self::getDatabase()->{$name}(...$arguments); - } - - public static function createImmutable(array|ConnectionInterface $credentialsOrConnection): CRUDInterface - { - if (isset(self::$databaseInstance)) { - return self::$databaseInstance; - } - - return self::$databaseInstance = new CRUD(new Database(($credentialsOrConnection instanceof ConnectionInterface) ? $credentialsOrConnection : new Connection($credentialsOrConnection), new QueryBuilder())); - } - - public static function connect(array|ConnectionInterface $credentialsOrConnection): CRUDInterface - { - return new CRUD(new Database((($credentialsOrConnection instanceof ConnectionInterface) ? $credentialsOrConnection : new Connection($credentialsOrConnection)), new QueryBuilder())); - } - - public static function getDatabase(): CRUDInterface - { - return self::$databaseInstance; - } - -} diff --git a/src/Model.php b/src/Model.php new file mode 100644 index 0000000..0d82c40 --- /dev/null +++ b/src/Model.php @@ -0,0 +1,8 @@ +setUp($data); - } - - /** - * @param $name - * @param $arguments - * @return mixed - * @throws EntityNotMethod - */ - public function __call($name, $arguments) - { - if (str_ends_with($name, 'Attribute') === false) { - throw new EntityNotMethod($name); - } - - $attr = Helper::camelCaseToSnakeCase(substr($name, 3, -9)); - - return match (substr($name, 0, 3)) { - 'get' => $this->__attributes[$attr] ?? null, - 'set' => $this->__attributes[$attr] = $arguments[0], - default => throw new EntityNotMethod($name) - }; - } - - public function __set($name, $value) - { - $methodName = 'set' . Helper::snakeCaseToPascalCase($name) . 'Attribute'; - if(method_exists($this, $methodName)){ - $this->{$methodName}($value); - return $value; - } - return $this->__attributes[$name] = $value; - } - - public function __get($name) - { - $methodName = 'get' . Helper::snakeCaseToPascalCase($name) . 'Attribute'; - if(method_exists($this, $methodName)){ - return $this->{$methodName}(); - } - return $this->__attributes[$name] ?? null; - } - - public function __isset($name) - { - return isset($this->__attributes[$name]); - } - - public function __unset($name) - { - if(isset($this->__attributes[$name])){ - unset($this->__attributes[$name]); - } - } - - public function __debugInfo() - { - return $this->__attributes; - } - - /** - * @inheritDoc - */ - public function toArray(): array - { - return $this->__attributes; - } - - /** - * @inheritDoc - */ - public function getAttributes(): array - { - return $this->toArray(); - } - - /** - * @param array|null $data - * @return $this - */ - protected function setUp(?array $data = null): self - { - $this->syncOriginal() - ->fill($data); - return $this; - } - - /** - * @param array|null $data - * @return $this - */ - protected function fill(?array $data = null): self - { - if($data !== null){ - foreach ($data as $key => $value) { - $this->__set($key, $value); - } - } - return $this; - } - - /** - * @return $this - */ - protected function syncOriginal(): self - { - $this->__attributesOriginal = $this->__attributes; - return $this; - } - -} diff --git a/src/ORM/Exceptions/DeletableException.php b/src/ORM/Exceptions/DeletableException.php deleted file mode 100644 index 67c7696..0000000 --- a/src/ORM/Exceptions/DeletableException.php +++ /dev/null @@ -1,7 +0,0 @@ -schema)) { - $modelClass = get_called_class(); - $modelReflection = new ReflectionClass($modelClass); - $this->schema = Helper::camelCaseToSnakeCase($modelReflection->getShortName()); - unset($modelClass, $modelReflection); - } - if ($this->useSoftDeletes !== false && empty($this->deletedField)) { - throw new ModelException('There must be a delete column to use soft delete.'); - } - - $this->crud = empty($this->credentials) ? DB::getDatabase() : DB::connect($this->credentials); - } - - public function __call(string $name, array $arguments) - { - $res = $this->crud->{$name}(...$arguments); - - return ($res instanceof CRUDInterface) ? $this : $res; - } - - /** - * @inheritDoc - */ - public function create(array $set = []): bool - { - if (!$this->writable) { - throw new WritableException(); - } - - !empty($this->createdField) && $set[$this->createdField] = date($this->timestampFormat); - - return $this->crud->create($this->schema, $set); - } - - /** - * @inheritDoc - */ - public function createBatch(array $set = []): bool - { - if (!$this->writable) { - throw new WritableException(); - } - $createdField = $this->createdField; - if (!empty($createdField) && !empty($set)) { - foreach ($set as &$row) { - $row[$createdField] = date($this->timestampFormat); - } - } - - return $this->crud->createBatch($this->schema, $set); - } - - /** - * @inheritDoc - */ - public function read(array $selector = [], array $conditions = [], array $parameters = []): ResultInterface - { - if (!$this->readable) { - throw new ReadableException(); - } - if ($this->useSoftDeletes) { - if ($this->isOnlyDelete) { - $this->onlyDeleted(); - } else { - $this->ignoreDeleted(); - } - $this->isOnlyDelete = false; - } - - return $this->crud - ->read($this->schema, $selector, $conditions, $parameters) - ->asClass($this->entity); - } - - /** - * @inheritDoc - */ - public function readOne(array $selector = [], array $conditions = [], array $parameters = []): ResultInterface - { - if (!$this->readable) { - throw new ReadableException(); - } - if ($this->useSoftDeletes) { - if ($this->isOnlyDelete) { - $this->onlyDeleted(); - } else { - $this->ignoreDeleted(); - } - $this->isOnlyDelete = false; - } - - return $this->crud - ->readOne($this->schema, $selector, $conditions, $parameters) - ->asClass($this->entity); - } - - /** - * @inheritDoc - */ - public function update(array $set = []): bool - { - if (!$this->updatable) { - throw new UpdatableException(); - } - - if (!empty($this->schemaId) && isset($set[$this->schemaId])) { - $this->where($this->schemaId, $set[$this->schemaId]); - unset($set[$this->schemaId]); - } - - !empty($this->updatedField) && $set[$this->updatedField] = date($this->timestampFormat); - - $this->ignoreDeleted(); - - return $this->crud->update($this->schema, $set); - } - - /** - * @inheritDoc - */ - public function updateBatch(array $set = [], ?string $referenceColumn = null): bool - { - if (!$this->updatable) { - throw new UpdatableException(); - } - $updatedField = $this->updatedField; - if (!empty($updatedField) && !empty($set)) { - foreach ($set as &$row) { - $row[$updatedField] = date($this->timestampFormat); - } - } - $this->ignoreDeleted(); - - return $this->crud->updateBatch($this->schema, $set, $referenceColumn ?? $this->schemaId); - } - - /** - * @inheritDoc - */ - public function delete(?array $conditions = null, bool $purge = false): bool - { - if (!$this->deletable) { - throw new DeletableException(); - } - if ($this->useSoftDeletes && $purge === false) { - $this->ignoreDeleted() - ->set($this->deletedField, date($this->timestampFormat)); - - if (!empty($conditions)) { - foreach ($conditions as $column => $value) { - if (is_string($column)) { - $this->crud->getQueryBuilder()->where($column, $value); - } else { - $this->crud->getQueryBuilder()->where($value); - } - } - } - - return $this->crud->update($this->schema); - } - - return $this->crud->delete($this->schema, $conditions); - } - - /** - * @inheritDoc - */ - public function save(EntityInterface $entity): bool - { - $data = $entity->toArray(); - - return !empty($this->schemaId) && isset($data[$this->schemaId]) ? $this->update($data) : $this->create($data); - } - - /** - * @inheritDoc - */ - public function ignoreDeleted(): self - { - $this->useSoftDeletes && $this->crud->getQueryBuilder() - ->whereIsNull($this->deletedField); - - return $this; - } - - /** - * @inheritDoc - */ - public function onlyDeleted(): self - { - $this->useSoftDeletes && $this->crud->getQueryBuilder() - ->whereIsNotNull($this->deletedField); - - return $this; - } - -} diff --git a/src/QueryBuilder/Exceptions/QueryBuilderException.php b/src/QueryBuilder/Exceptions/QueryBuilderException.php deleted file mode 100644 index 2d5c661..0000000 --- a/src/QueryBuilder/Exceptions/QueryBuilderException.php +++ /dev/null @@ -1,8 +0,0 @@ -parameters = []; - } - - /** - * @inheritDoc - */ - public function set(string $key, mixed $value): self - { - $this->parameters[':' . ltrim(str_replace('.', '', $key), ':')] = $value; - - return $this; - } - - /** - * @inheritDoc - */ - public function add(RawQuery|string $key, mixed $value): string - { - if ($value === null) { - return 'NULL'; - } - if ($key instanceof RawQuery) { - $key = md5((string)$key); - } - $originKey = ltrim(str_replace('.', '', $key), ':'); - $i = 0; - do { - $key = ':' . ($i === 0 ? $originKey : $originKey . '_' . $i); - ++$i; - $hasParameter = isset($this->parameters[$key]); - } while($hasParameter); - - $this->parameters[$key] = $value; - - return $key; - } - - /** - * @inheritDoc - */ - public function merge(array|ParameterInterface ...$arrays): self - { - foreach ($arrays as $array) { - if ($array instanceof ParameterInterface) { - $array = $array->all(); - } - foreach ($array as $key => $value) { - $this->set($key, $value); - } - } - - return $this; - } - - /** - * @inheritDoc - */ - public function get(?string $key = null, mixed $default = null): mixed - { - if ($key === null) { - return $this->parameters; - } - - $key = ':' . ltrim($key, ':'); - if (isset($this->parameters[$key])) { - return $this->parameters[$key]; - } - - return ($default instanceof Closure) ? call_user_func_array($default, []) : $default; - } - - /** - * @inheritDoc - */ - public function all(): array - { - return $this->parameters; - } - - /** - * @inheritDoc - */ - public function reset(): self - { - $this->parameters = []; - - return $this; - } - -} diff --git a/src/QueryBuilder/QueryBuilder.php b/src/QueryBuilder/QueryBuilder.php deleted file mode 100644 index 19ee7e7..0000000 --- a/src/QueryBuilder/QueryBuilder.php +++ /dev/null @@ -1,1455 +0,0 @@ - [], - 'table' => [], - 'join' => [], - 'where' => [ - 'AND' => [], - 'OR' => [], - ], - 'having' => [ - 'AND' => [], - 'OR' => [], - ], - 'group_by' => [], - 'order_by' => [], - 'offset' => null, - 'limit' => null, - 'set' => [], - 'on' => [ - 'AND' => [], - 'OR' => [], - ], - ]; - - protected array $structure; - - protected ParameterInterface $parameters; - - public function __construct() - { - $this->structure = self::STRUCTURE; - $this->parameters = new Parameters(); - } - - /** - * @return string - * @throws QueryGeneratorException - */ - public function __toString(): string - { - if (empty($this->structure['set'])) { - return $this->generateSelectQuery(); - } - - $isBatch = $this->isBatch(); - $isInsert = empty($this->structure['where']['OR']) && empty($this->structure['where']['AND']) && empty($this->structure['having']['OR']) && empty($this->structure['having']['AND']); - - if ($isInsert) { - return $isBatch ? $this->generateBatchInsertQuery() : $this->generateInsertQuery(); - } - - return $this->generateUpdateQuery(); - } - - /** - * @inheritDoc - */ - public function newBuilder(): self - { - return new self(); - } - - /** - * @param string[]|string|null $ignoreOrCare - * @param null|bool $isIgnore - * @return $this - */ - public function resetStructure(null|array|string $ignoreOrCare = null, ?bool $isIgnore = null): self - { - if ($ignoreOrCare === null) { - $this->structure = self::STRUCTURE; - } else { - if (is_string($ignoreOrCare)) { - $ignoreOrCare = [$ignoreOrCare]; - } - - $newStructure = self::STRUCTURE; - foreach ($ignoreOrCare as $key) { - if (!isset($this->structure[$key])) { - continue; - } - if ($isIgnore) { - $newStructure[$key] = $this->structure[$key]; - } else { - $newStructure[$key] = self::STRUCTURE[$key] ?? []; - } - } - - $this->structure = $newStructure; - } - - return $this; - } - - public function clone(): self - { - return (clone $this); - } - - /** - * @inheritDoc - */ - public function importQB(array $structure, bool $merge = false): self - { - $this->structure = array_merge(($merge ? $this->structure : self::STRUCTURE), $structure); - - return $this; - } - - /** - * @inheritDoc - */ - public function exportQB(): array - { - return $this->structure; - } - - /** - * @inheritDoc - */ - public function getParameter(): ParameterInterface - { - return $this->parameters; - } - - /** - * @inheritDoc - */ - public function setParameter(string $key, mixed $value): self - { - $this->parameters->set($key, $value); - - return $this; - } - - /** - * @inheritDoc - */ - public function setParameters(array $parameters = []): self - { - foreach ($parameters as $key => $value) { - $this->parameters->set($key, $value); - } - - return $this; - } - - /** - * @inheritDoc - */ - public function select(...$columns): self - { - foreach ($columns as $column) { - $column = (string)$column; - $this->structure['select'][] = $column; - } - - return $this; - } - - /** - * @inheritDoc - */ - public function clearSelect(): self - { - $this->structure['select'] = []; - - return $this; - } - - /** - * @inheritDoc - */ - public function selectCount(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'COUNT(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectCountDistinct(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'COUNT(DISTINCT ' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - return $this; - } - - /** - * @inheritDoc - */ - public function selectMax(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'MAX(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectMin(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'MIN(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectAvg(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'AVG(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectAs(RawQuery|string $column, string $alias): self - { - $this->structure['select'][] = $column . ' AS ' . $alias; - - return $this; - } - - /** - * @inheritDoc - */ - public function selectUpper(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'UPPER(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectLower(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'LOWER(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectLength(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'LENGTH(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectMid(RawQuery|string $column, int $offset, int $length, ?string $alias = null): self - { - $this->structure['select'][] = 'MID(' . $column . ', ' . $offset . ', ' . $length . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectLeft(RawQuery|string $column, int $length, ?string $alias = null): self - { - $this->structure['select'][] = 'LEFT(' . $column . ', ' . $length . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectRight(RawQuery|string $column, int $length, ?string $alias = null): self - { - $this->structure['select'][] = 'RIGHT(' . $column . ', ' . $length . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectDistinct(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'DISTINCT(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectCoalesce(RawQuery|string $column, $default = '0', ?string $alias = null): self - { - $this->structure['select'][] = 'COALESCE(' . $column . ', ' . $default . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectSum(RawQuery|string $column, ?string $alias = null): self - { - $this->structure['select'][] = 'SUM(' . $column . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function selectConcat(array $columns, ?string $alias = null): self - { - foreach ($columns as &$column) { - $column = (string)$column; - } - $this->structure['select'][] = 'CONCAT(' . implode(', ', $columns) . ')' - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this; - } - - /** - * @inheritDoc - */ - public function from(RawQuery|string $table, ?string $alias = null): self - { - $this->structure['table'] = []; - - return $this->addFrom($table, $alias); - } - - /** - * @inheritDoc - */ - public function addFrom(RawQuery|string $table, ?string $alias = null): self - { - $table = $table . ($alias !== null ? ' AS ' . $alias : ''); - if (!in_array($table, $this->structure['table'], true)) { - $this->structure['table'][] = $table; - } - - return $this; - } - - /** - * @inheritDoc - */ - public function table(RawQuery|string $table): self - { - $this->structure['table'] = [(string)$table]; - - return $this; - } - - /** - * @inheritDoc - */ - public function groupBy(string|RawQuery|array ...$columns): self - { - foreach ($columns as $column) { - if (is_array($column)) { - $this->groupBy(...$column); - continue; - } - - $column = (string)$column; - if (!in_array($column, $this->structure['group_by'])) { - $this->structure['group_by'][] = $column; - } - } - - return $this; - } - - /** - * @inheritDoc - */ - public function join(RawQuery|string $table, RawQuery|Closure|string $onStmt = null, string $type = 'INNER'): self - { - $table = (string)$table; - - if ($onStmt instanceof Closure) { - $builder = $this->clone()->resetStructure(); - $onStmt = call_user_func_array($onStmt, [&$builder]); - if ($onStmt === null) { - if ($where = $builder->__generateWhereQuery()) { - $this->where($this->raw($where)); - } - if ($having = $builder->__generateHavingQuery()) { - if (str_starts_with($having, ' HAVING ')) { - $having = substr($having, 8); - } - $this->having($this->raw($having)); - } - $onStmt = $builder->__generateOnQuery(); - } - } - - $type = trim(strtoupper($type)); - switch ($type) { - case 'SELF': - $this->addFrom($table); - $this->where(is_string($onStmt) ? $this->raw($onStmt) : $onStmt); - break; - case 'NATURAL': - case 'NATURAL JOIN': - $this->structure['join'][$table] = 'NATURAL JOIN ' . $table; - break; - default: - $this->structure['join'][$table] = trim($type . ' JOIN ' . $table . ' ON ' . $onStmt); - } - - return $this; - } - - /** - * @inheritDoc - */ - public function selfJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self - { - return $this->join($table, $onStmt, 'SELF'); - } - - /** - * @inheritDoc - */ - public function innerJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self - { - return $this->join($table, $onStmt); - } - - /** - * @inheritDoc - */ - public function leftJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self - { - return $this->join($table, $onStmt, 'LEFT'); - } - - /** - * @inheritDoc - */ - public function rightJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self - { - return $this->join($table, $onStmt, 'RIGHT'); - } - - /** - * @inheritDoc - */ - public function leftOuterJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self - { - return $this->join($table, $onStmt, 'LEFT OUTER'); - } - - /** - * @inheritDoc - */ - public function rightOuterJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self - { - return $this->join($table, $onStmt, 'RIGHT OUTER'); - } - - /** - * @inheritDoc - */ - public function naturalJoin(RawQuery|string $table, RawQuery|Closure|string $onStmt): self - { - return $this->join($table, null, 'NATURAL'); - } - - /** - * @inheritDoc - */ - public function orderBy(RawQuery|string $column, string $soft = 'ASC'): self - { - $soft = trim(strtoupper($soft)); - if (!in_array($soft, ['ASC', 'DESC'], true)) { - throw new InvalidArgumentException('It can only sort as ASC or DESC.'); - } - $orderBy = trim((string)$column) . ' ' . $soft; - - !in_array($orderBy, $this->structure['order_by'], true) && $this->structure['order_by'][] = $orderBy; - - return $this; - } - - /** - * @inheritDoc - */ - public function where(RawQuery|string $column, mixed $operator = '=', mixed $value = null, string $logical = 'AND'): self - { - - $this->whereOrHavingPrepare($operator, $value, $logical); - - $this->structure['where'][$logical][] = $this->whereOrHavingStatementPrepare($column, $operator, $value); - - return $this; - } - - /** - * @inheritDoc - */ - public function having(RawQuery|string $column, mixed $operator = '=', mixed $value = null, string $logical = 'AND'): self - { - $this->whereOrHavingPrepare($operator, $value, $logical); - $this->structure['having'][$logical][] = $this->whereOrHavingStatementPrepare($column, $operator, $value); - - return $this; - } - - /** - * @inheritDoc - */ - public function on(RawQuery|string $column, mixed $operator = '=', mixed $value = null, string $logical = 'AND'): self - { - $this->whereOrHavingPrepare($operator, $value, $logical); - - $this->structure['on'][$logical][] = $this->whereOrHavingStatementPrepare($column, $operator, $value); - - return $this; - } - - /** - * @inheritDoc - */ - public function set(RawQuery|array|string $column, mixed $value = null, bool $strict = true): self - { - return $this->addSet($column, $value, $strict); - } - - /** - * @inheritDoc - */ - public function addSet(RawQuery|array|string $column, mixed $value = null, bool $strict = true): self - { - if (is_array($column) && $value === null) { - $set = []; - foreach ($column as $name => $value) { - $name = (string)$name; - $set[$name] = $this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($name, $value); - } - $this->structure['set'][] = $set; - - return $this; - } - - $column = (string)$column; - $value = $this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value); - - $this->structure['set'][][$column] = $value; - - return $this; - } - - /** - * @inheritDoc - */ - public function andWhere(RawQuery|string $column, mixed $operator = '=', mixed $value = null): self - { - return $this->where($column, $operator, $value); - } - - /** - * @inheritDoc - */ - public function orWhere(RawQuery|string $column, mixed $operator = '=', mixed $value = null): self - { - return $this->where($column, $operator, $value, 'OR'); - } - - /** - * @inheritDoc - */ - public function between(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null, string $logical = 'AND'): self - { - if (is_array($firstValue) && count($firstValue) == 2 && $lastValue === null) { - $value = $firstValue; - } else { - $value = [$firstValue, $lastValue]; - } - - return $this->where($column, 'BETWEEN', $value, $logical); - } - - /** - * @inheritDoc - */ - public function orBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null): self - { - return $this->between($column, [$firstValue, $lastValue], 'OR'); - } - - /** - * @inheritDoc - */ - public function andBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null): self - { - return $this->between($column, $firstValue, $lastValue); - } - - /** - * @inheritDoc - */ - public function notBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null, string $logical = 'AND'): self - { - if (is_array($firstValue) && count($firstValue) == 2 && $lastValue === null) { - $value = $firstValue; - } else { - $value = [$firstValue, $lastValue]; - } - - return $this->where($column, 'NOT BETWEEN', $value, $logical); - } - - /** - * @inheritDoc - */ - public function orNotBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null): self - { - return $this->notBetween($column, $firstValue, $lastValue, 'OR'); - } - - /** - * @inheritDoc - */ - public function andNotBetween(RawQuery|string $column, mixed $firstValue = null, mixed $lastValue = null): self - { - return $this->notBetween($column, $firstValue, $lastValue); - } - - /** - * @inheritDoc - */ - public function findInSet(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->where($column, 'FIND_IN_SET', $value, $logical); - } - - /** - * @inheritDoc - */ - public function andFindInSet(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'FIND_IN_SET', $value); - } - - /** - * @inheritDoc - */ - public function orFindInSet(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'FIND_IN_SET', $value, 'OR'); - } - - /** - * @inheritDoc - */ - public function notFindInSet(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->where($column, 'NOT FIND_IN_SET', $value, $logical); - } - - /** - * @inheritDoc - */ - public function andNotFindInSet(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'NOT FIND_IN_SET', $value); - } - - /** - * @inheritDoc - */ - public function orNotFindInSet(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'NOT FIND_IN_SET', $value, 'OR'); - } - - /** - * @inheritDoc - */ - public function whereIn(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->where($column, 'IN', $value, $logical); - } - - /** - * @inheritDoc - */ - public function whereNotIn(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->where($column, 'NOT IN', $value, $logical); - } - - /** - * @inheritDoc - */ - public function orWhereIn(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'IN', $value, 'OR'); - } - - /** - * @inheritDoc - */ - public function orWhereNotIn(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'NOT IN', $value, 'OR'); - } - - /** - * @inheritDoc - */ - public function andWhereIn(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'IN', $value); - } - - /** - * @inheritDoc - */ - public function andWhereNotIn(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'IN', $value); - } - - /** - * @inheritDoc - */ - public function regexp(RawQuery|string $column, RawQuery|string $value, string $logical = 'AND'): self - { - return $this->where($column, 'REGEXP', $value, $logical); - } - - /** - * @inheritDoc - */ - public function andRegexp(RawQuery|string $column, RawQuery|string $value): self - { - return $this->where($column, 'REGEXP', $value); - } - - /** - * @inheritDoc - */ - public function orRegexp(RawQuery|string $column, RawQuery|string $value): self - { - return $this->where($column, 'REGEXP', $value, 'OR'); - } - - /** - * @inheritDoc - */ - public function soundex(RawQuery|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->where($column, 'SOUNDEX', $value, $logical); - } - - /** - * @inheritDoc - */ - public function andSoundex(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'SOUNDEX', $value); - } - - /** - * @inheritDoc - */ - public function orSoundex(RawQuery|string $column, mixed $value = null): self - { - return $this->where($column, 'SOUNDEX', $value, 'OR'); - } - - /** - * @inheritDoc - */ - public function whereIsNull(RawQuery|string $column, string $logical = 'AND'): self - { - return $this->where($column, 'IS', null, $logical); - } - - /** - * @inheritDoc - */ - public function orWhereIsNull(RawQuery|string $column): self - { - return $this->where($column, 'IS', null, 'OR'); - } - - /** - * @inheritDoc - */ - public function andWhereIsNull(RawQuery|string $column): self - { - return $this->where($column, 'IS'); - } - - /** - * @inheritDoc - */ - public function whereIsNotNull(RawQuery|string $column, string $logical = 'AND'): self - { - return $this->where($column, 'IS NOT', null, $logical); - } - - /** - * @inheritDoc - */ - public function orWhereIsNotNull(RawQuery|string $column): self - { - return $this->where($column, 'IS NOT', null, 'OR'); - } - - /** - * @inheritDoc - */ - public function andWhereIsNotNull(RawQuery|string $column): self - { - return $this->where($column, 'IS NOT'); - } - - /** - * @inheritDoc - */ - public function offset(int $offset = 0): self - { - $this->structure['offset'] = (int)abs($offset); - - return $this; - } - - /** - * @inheritDoc - */ - public function limit(int $limit): self - { - $this->structure['limit'] = (int)abs($limit); - - return $this; - } - - /** - * @inheritDoc - */ - public function like(RawQuery|array|string $column, mixed $value = null, string $type = 'both', string $logical = 'AND'): self - { - $operator = match (strtolower($type)) { - 'before', 'start' => 'START LIKE', - 'after', 'end' => 'END LIKE', - default => 'LIKE' - }; - - return $this->where($column, $operator, $value, $logical); - } - - /** - * @inheritDoc - */ - public function orLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both'): self - { - return $this->like($column, $value, $type); - } - - /** - * @inheritDoc - */ - public function andLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both'): self - { - return $this->like($column, $value, $type, 'OR'); - } - - /** - * @inheritDoc - */ - public function notLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both', string $logical = 'AND'): self - { - $operator = match (strtolower($type)) { - 'before', 'start' => 'NOT START LIKE', - 'after', 'end' => 'NOT END LIKE', - default => 'NOT LIKE' - }; - - return $this->where($column, $operator, $value, $logical); - } - - /** - * @inheritDoc - */ - public function orNotLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both'): self - { - return $this->notLike($column, $value, $type, 'OR'); - } - - /** - * @inheritDoc - */ - public function andNotLike(RawQuery|array|string $column, mixed $value = null, string $type = 'both'): self - { - return $this->notLike($column, $value, $type); - } - - /** - * @inheritDoc - */ - public function startLike(RawQuery|array|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->like($column, $value, 'before', $logical); - } - - /** - * @inheritDoc - */ - public function orStartLike(RawQuery|array|string $column, mixed $value = null): self - { - return $this->like($column, $value, 'before', 'OR'); - } - - /** - * @inheritDoc - */ - public function andStartLike(RawQuery|array|string $column, mixed $value = null): self - { - return $this->like($column, $value, 'before'); - } - - /** - * @inheritDoc - */ - public function notStartLike(RawQuery|array|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->notLike($column, $value, 'before', $logical); - } - - /** - * @inheritDoc - */ - public function orStartNotLike(RawQuery|array|string $column, mixed $value = null): self - { - return $this->notLike($column, $value, 'before', 'OR'); - } - - /** - * @inheritDoc - */ - public function andStartNotLike(RawQuery|array|string $column, mixed $value = null): self - { - return $this->notLike($column, $value, 'before'); - } - - /** - * @inheritDoc - */ - public function endLike(RawQuery|array|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->like($column, $value, 'after', $logical); - } - - /** - * @inheritDoc - */ - public function orEndLike(RawQuery|array|string $column, mixed $value = null): self - { - return $this->like($column, $value, 'after', 'OR'); - } - - /** - * @inheritDoc - */ - public function andEndLike(RawQuery|array|string $column, mixed $value = null): self - { - return $this->like($column, $value, 'after'); - } - - /** - * @inheritDoc - */ - public function notEndLike(RawQuery|array|string $column, mixed $value = null, string $logical = 'AND'): self - { - return $this->notLike($column, $value, 'after', $logical); - } - - /** - * @inheritDoc - */ - public function orEndNotLike(RawQuery|array|string $column, mixed $value = null): self - { - return $this->notLike($column, $value, 'after', 'OR'); - } - - /** - * @inheritDoc - */ - public function andEndNotLike(RawQuery|array|string $column, mixed $value = null): self - { - return $this->notLike($column, $value, 'after'); - } - - /** - * @inheritDoc - */ - public function subQuery(Closure $closure, ?string $alias = null, bool $isIntervalQuery = true): RawQuery - { - $builder = $this->clone()->resetStructure(); - - call_user_func_array($closure, [&$builder]); - if ($alias !== null && $isIntervalQuery !== TRUE) { - throw new QueryBuilderException('To define alias to a subquery, it must be an inner query.'); - } - - $rawQuery = ($isIntervalQuery ? '(' : '') - . $builder->generateSelectQuery() - . ($isIntervalQuery ? ')' : '') - . ($alias !== null ? ' AS ' . $alias : ''); - - return $this->raw($rawQuery); - } - - /** - * @inheritDoc - */ - public function group(Closure $closure, string $logical = 'AND'): self - { - $logical = str_replace(['&&', '||'], ['AND', 'OR'], strtoupper($logical)); - if(!in_array($logical, ['AND', 'OR'], true)){ - throw new QueryBuilderException('Logical operator OR, AND, && or || it could be.'); - } - - $builder = $this->clone(); - call_user_func_array($closure, [$builder->resetStructure()]); - - - - foreach (['where', 'on', 'having'] as $stmt) { - $statement = $builder->__generateStructure($stmt); - !empty($statement) && $this->structure[$stmt][$logical][] = '(' . $statement . ')'; - } - - return $this; - } - - /** - * @inheritDoc - */ - public function raw(mixed $rawQuery): RawQuery - { - return new RawQuery($rawQuery); - } - - /** - * @return string - * @throws QueryGeneratorException - */ - public function generateInsertQuery(): string - { - $columns = []; - $values = []; - $set = array_merge(...$this->structure['set']); - foreach ($set as $column => $value) { - $columns[] = $column; - $values[] = $value; - } - if (empty($columns)) { - throw new QueryGeneratorException('The data set for the insert could not be found.'); - } - - return 'INSERT INTO' - . ' ' . $this->__generateSchemaName() . ' ' - . '(' . implode(', ', $columns) . ')' - . ' VALUES ' - . '(' . implode(', ', $values) . ');'; - } - - /** - * @return string - * @throws QueryGeneratorException - */ - public function generateBatchInsertQuery(): string - { - $columns = array_keys(array_merge(...$this->structure['set'])); - if (empty($columns)) { - throw new QueryGeneratorException('The data set for the insert could not be found.'); - } - $values = []; - foreach ($this->structure['set'] as $set) { - $value = []; - foreach ($columns as $column) { - $value[$column] = $set[$column] ?? 'NULL'; - } - $values[] = '(' . implode(', ', $value) . ')'; - } - - return 'INSERT INTO' - . ' ' . $this->__generateSchemaName() . ' ' - . '(' . implode(', ', $columns) . ')' - . ' VALUES ' - . implode(', ', $values) . ';'; - } - - /** - * @return string - * @throws QueryGeneratorException - */ - public function generateDeleteQuery(): string - { - return 'DELETE FROM' - . ' ' - . $this->__generateSchemaName() - . ' WHERE ' - . (($where = $this->__generateWhereQuery()) !== null ? $where : '1') - . ($this->__generateLimitQuery() ?? ''); - } - - /** - * @param array $selector - * @param array $conditions - * @return string - */ - public function generateSelectQuery(array $selector = [], array $conditions = []): string - { - !empty($selector) && $this->select(...$selector); - if (!empty($conditions)) { - foreach ($conditions as $column => $value) { - if (is_string($column)) { - $this->where($column, $value); - } else { - $this->where($value); - } - } - } - - return 'SELECT ' - . (empty($this->structure['select']) ? '*' : implode(', ', $this->structure['select'])) - . ' FROM ' - . implode(', ', $this->structure['table']) - . (!empty($this->structure['join']) ? ' ' . implode(' ', $this->structure['join']) : '') - . ' WHERE ' - . (($where = $this->__generateWhereQuery()) ? $where : '1') - . (!empty($this->structure['group_by']) ? ' GROUP BY ' . implode(', ', $this->structure['group_by']) : '') - . ($this->__generateHavingQuery() ?? '') - . (!empty($this->structure['order_by']) ? ' ORDER BY ' . implode(', ', $this->structure['order_by']) : '') - . ($this->__generateLimitQuery() ?? ''); - } - - /** - * @return string - * @throws QueryGeneratorException - */ - public function generateUpdateQuery(): string - { - $set = array_merge(...$this->structure['set']); - $updateSet = []; - foreach ($set as $column => $value) { - $updateSet[] = $column . ' = ' . $value; - } - if (empty($updateSet)) { - throw new QueryGeneratorException('The data set for the insert could not be found.'); - } - - return 'UPDATE ' . $this->__generateSchemaName() - . ' SET ' . implode(', ', $updateSet) - . ' WHERE ' - . (($where = $this->__generateWhereQuery()) ? $where : '1') - . ($this->__generateHavingQuery() ?? '') - . ($this->__generateLimitQuery() ?? ''); - } - - /** - * @param string $referenceColumn - * @return string - * @throws QueryGeneratorException - */ - public function generateUpdateBatchQuery(string $referenceColumn): string - { - $update = []; - $data = $this->structure['set']; - $updateData = $columns = $where = []; - foreach ($data as $set) { - if (!isset($set[$referenceColumn])) { - throw new QueryGeneratorException('The reference column does not exist in one or more of the set arrays.'); - } - $setData = []; - $where[] = $set[$referenceColumn]; - unset($set[$referenceColumn]); - foreach ($set as $key => $value) { - $setData[$key] = $value; - (!in_array($key, $columns)) && $columns[] = $key; - } - $updateData[] = $setData; - } - foreach ($columns as $column) { - $syntax = $column . ' = CASE'; - foreach ($updateData as $key => $values) { - if (!array_key_exists($column, $values)) { - continue; - } - $syntax .= ' WHEN ' . $referenceColumn . ' = ' - . ($this->isSQLParameterOrFunction($where[$key]) ? $where[$key] : $this->parameters->add($referenceColumn, $where[$key])) - . ' THEN ' - . $values[$column]; - } - $update[] = $syntax . ' ELSE ' . $column .' END'; - } - - $this->whereIn($referenceColumn, $where); - - return 'UPDATE ' . $this->__generateSchemaName() - . ' SET ' - . implode(', ', $update) - . ' WHERE ' - . (($where = $this->__generateWhereQuery()) ? $where : '1') - . ($this->__generateHavingQuery() ?? '') - . ($this->__generateLimitQuery() ?? ''); - } - - protected function isSQLParameter($value): bool - { - return (is_string($value)) && ($value === '?' || preg_match('/^:[(\w)]+$/', $value)); - } - - protected function isSQLParameterOrFunction($value): bool - { - return ((is_string($value)) && ( - $value === '?' - || preg_match('/^:[(\w)]+$/', $value) - || preg_match('/^[a-zA-Z_]+[.]+[a-zA-Z_]+$/', $value) - || preg_match('/^[a-zA-Z_]+\(\)$/', $value) - )) || ($value instanceof RawQuery) || is_int($value); - } - - public function isBatch(): bool - { - foreach ($this->structure['set'] as $set) { - if (is_array($set) && count($set) > 1) { - return true; - } - } - - return false; - } - - private function whereOrHavingStatementPrepare($column, $operator, $value): string - { - $operator = trim($operator); - $column = (string)$column; - - if ($value !== null && in_array($operator, [ - '=', '!=', '>', '<', '>=', '<=', '<>', - '+', '-', '*', '/', '%', - '+=', '-=', '*=', '/=', '%=', '&=', '^-=', '|*=' - ], true)) { - return $column . ' ' . $operator . ' ' - . ($this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value)); - } - $upperCaseOperator = strtoupper($operator); - $searchOperator = str_replace([' ', '_'], '', $upperCaseOperator); - if ($value === null && !in_array($searchOperator, ['IS', 'ISNOT'])) { - return $column; - } - - switch ($searchOperator) { - case 'IS': - return $column . ' IS ' - . ((($value === null) ? 'NULL' : ($this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value)))); - case 'ISNOT': - return $column . ' IS NOT ' - . ((($value === null) ? 'NULL' : ($this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value)))); - case 'LIKE': - case 'NOTLIKE': - case 'STARTLIKE': - case 'NOTSTARTLIKE': - case 'ENDLIKE': - case 'NOTENDLIKE': - if (!$this->isSQLParameter($value)) { - $value = (in_array($searchOperator, ['LIKE', 'NOTLIKE', 'STARTLIKE', 'NOTSTARTLIKE']) ? '%' : '') - . $value - . (in_array($searchOperator, ['LIKE', 'NOTLIKE', 'ENDLIKE', 'NOTENDLIKE']) ? '%' : ''); - - $value = $this->parameters->add($column, $value); - } - - return $column - . (in_array($searchOperator, ['NOTSTARTLIKE', 'NOTLIKE', 'NOTENDLIKE']) ? ' NOT' : '') - . ' LIKE ' . $value; - case 'BETWEEN': - case 'NOTBETWEEN': - return $column . ' ' - . ($searchOperator === 'NOTBETWEEN' ? 'NOT ' : '') - . 'BETWEEN ' - . ($this->isSQLParameterOrFunction($value[0]) ? $value[0] : $this->parameters->add($column, $value[0])) - . ' AND ' - . ($this->isSQLParameterOrFunction($value[1]) ? $value[1] : $this->parameters->add($column, $value[1])); - case 'IN': - case 'NOTIN': - if (is_array($value)) { - $values = []; - array_map(function ($item) use (&$values, $column) { - if (is_numeric($item)) { - $values[] = $item; - } else { - $values[] = $this->isSQLParameterOrFunction($item) ? $item : $this->parameters->add($column, $item); - } - }, array_unique($value)); - $value = '(' . implode(', ', $values) . ')'; - } - return $column - . ($searchOperator === 'NOTIN' ? ' NOT' : '') - . ' IN ' . $value; - case 'REGEXP': - return $column . ' REGEXP ' - . ($this->isSQLParameterOrFunction($value) ? $value : $this->parameters->add($column, $value)); - case 'FINDINSET': - case 'NOTFINDINSET': - if (is_array($value)) { - $value = implode(', ', $value); - } elseif ($this->isSQLParameterOrFunction($value)) { - $value = $this->parameters->add($column, $value); - } - return ($searchOperator === 'NOTFINDINSET' ? 'NOT ' : '') - . 'FIND_IN_SET(' . $value . ', ' . $column . ')'; - case 'SOUNDEX': - if (!$this->isSQLParameterOrFunction($value)) { - $value = $this->parameters->add($column, $value); - } - return "SOUNDEX(" . $column . ") LIKE CONCAT('%', TRIM(TRAILING '0' FROM SOUNDEX(" . $value . ")), '%')"; - default: - if ($value === null && preg_match('/([\w_]+)\((.+)\)$/iu', $column, $matches) !== FALSE) { - return strtoupper($matches[1]) . '(' . $matches[2] . ')'; - } - return $column . ' ' . $operator . ' ' . $this->parameters->add($column, $value); - } - } - - /** - * @return string - * @throws QueryGeneratorException - */ - private function __generateSchemaName(): string - { - if (!empty($this->structure['table'])) { - $table = end($this->structure['table']); - } else { - throw new QueryGeneratorException('Table name not found when query.'); - } - - return $table; - } - - private function __generateLimitQuery(): ?string - { - if ($this->structure['limit'] === null && $this->structure['offset'] === null) { - return null; - } - $statement = ' '; - if ($this->structure['limit'] === null) { - $statement .= 'OFFSET ' . $this->structure['offset']; - } else { - $statement .= 'LIMIT ' - . ($this->structure['offset'] !== null ? $this->structure['offset'] . ', ' : '') - . $this->structure['limit']; - } - - return $statement; - } - - private function __generateOnQuery(): ?string - { - return $this->__generateStructure('on'); - } - - private function __generateHavingQuery(): ?string - { - $stmt = $this->__generateStructure('having'); - - return $stmt === null ? null : ' HAVING ' . $stmt; - } - - private function __generateWhereQuery(): ?string - { - return $this->__generateStructure('where'); - } - - private function __generateStructure(string $key): ?string - { - $isAndEmpty = empty($this->structure[$key]['AND']); - $isOrEmpty = empty($this->structure[$key]['OR']); - if ($isOrEmpty && $isAndEmpty) { - return null; - } - - return (!$isAndEmpty ? implode(' AND ', $this->structure[$key]['AND']) : '') - . (!$isAndEmpty && !$isOrEmpty ? ' AND ' : '') - . (!$isOrEmpty ? implode(' OR ', $this->structure[$key]['OR']) : ''); - } - - private function whereOrHavingPrepare(&$operator, &$value, &$logical): void - { - $logical = strtoupper(strtr($logical, [ - '&&' => 'AND', - '||' => 'OR', - ])); - if (!in_array($logical, ['AND', 'OR'], true)) { - throw new InvalidArgumentException('Logical operator OR, AND, && or || it could be.'); - } - - if ($value === null && !in_array($operator, [ - 'IS', 'IS NOT', - '=', '!=', '>', '<', '>=', '<=', '<>', - '+', '-', '*', '/', '%', - '+=', '-=', '*=', '/=', '%=', '&=', '^-=', '|*=' - ])) { - $value = $operator; - $operator = '='; - } - } - -} diff --git a/src/QueryBuilder/RawQuery.php b/src/QueryBuilder/RawQuery.php deleted file mode 100644 index 85b41f3..0000000 --- a/src/QueryBuilder/RawQuery.php +++ /dev/null @@ -1,53 +0,0 @@ -set($rawQuery); - } - - public function __toString(): string - { - return $this->get(); - } - - public function set(mixed $rawQuery): self - { - if (is_string($rawQuery)) { - $this->raw = $rawQuery; - } else if ($rawQuery instanceof Closure) { - $builder = new QueryBuilder(); - $res = call_user_func_array($rawQuery, [&$builder]); - if (is_string($res)) { - $this->raw = $res; - } else if (is_object($res) && method_exists($res, '__toString')) { - $this->raw = $res->__toString(); - } else { - $this->raw = $builder->__toString(); - } - } else { - $this->raw = (string)$rawQuery; - } - - return $this; - } - - public function get(): string - { - return $this->raw ?? ''; - } - - public static function raw($rawQuery): RawQuery - { - return new self($rawQuery); - } - -} diff --git a/src/Utils/Datatables.php b/src/Utils/Datatables.php index 32eb8ed..17f92b8 100644 --- a/src/Utils/Datatables.php +++ b/src/Utils/Datatables.php @@ -7,17 +7,18 @@ * @author Muhammet ŞAFAK * @copyright Copyright © 2022 Muhammet ŞAFAK * @license ./LICENSE MIT - * @version 2.0.7 + * @version 3.0 * @link https://www.muhammetsafak.com.tr */ namespace InitPHP\Database\Utils; -use \InitPHP\Database\DBAL\Interfaces\{DatabaseInterface, CRUDInterface}; -use InitPHP\Database\DBAL\Exceptions\SQLQueryExecuteException; -use InitPHP\Database\ORM\Interfaces\ModelInterface; -use InitPHP\Database\QueryBuilder\Interfaces\QueryBuilderInterface; use Closure; +use Throwable; +use InitORM\Database\Interfaces\DatabaseInterface; +use InitORM\QueryBuilder\Exceptions\QueryBuilderException; +use InitORM\QueryBuilder\QueryBuilderInterface; +use InitORM\ORM\Interfaces\ModelInterface; /** * @mixin QueryBuilderInterface @@ -25,7 +26,7 @@ class Datatables { - private DatabaseInterface|CRUDInterface|ModelInterface $db; + private DatabaseInterface|ModelInterface $db; private array $request; @@ -47,7 +48,7 @@ class Datatables private array $permanentSelect = []; - public function __construct(DatabaseInterface|ModelInterface|CRUDInterface $db) + public function __construct(DatabaseInterface|ModelInterface $db) { $this->request = array_merge($_GET ?? [], $_POST ?? []); @@ -71,7 +72,7 @@ public function __call(string $name, array $arguments) /** * @return string - * @throws SQLQueryExecuteException + * @throws Throwable */ public function __toString(): string { @@ -80,7 +81,7 @@ public function __toString(): string /** * @return array - * @throws SQLQueryExecuteException + * @throws Throwable */ public function toArray(): array { @@ -90,7 +91,7 @@ public function toArray(): array /** * @return $this - * @throws SQLQueryExecuteException + * @throws Throwable */ public function handle(): self { @@ -179,7 +180,7 @@ public function orderBySave(): self /** * @return int - * @throws SQLQueryExecuteException + * @throws Throwable */ private function getCount(): int { @@ -203,21 +204,21 @@ private function getCount(): int $this->db->{$process['method']}(...$process['arguments']); } $isGroupBy === false && $this->db->selectCount('*', 'data_length'); - $res = $this->db->get(); + $res = $this->db->read(); return $res->numRows() > 0 ? $res->asAssoc()->row()['data_length'] : 0; } /** * @return array - * @throws SQLQueryExecuteException + * @throws Throwable */ private function getResults(): array { foreach ($this->builder as $process) { $this->db->{$process['method']}(...$process['arguments']); } - $res = $this->db->get(); + $res = $this->db->read(); return $res->numRows() > 0 ? $res->asAssoc()->rows() : []; } @@ -254,6 +255,7 @@ private function orderQuery(): self /** * @return void + * @throws QueryBuilderException */ private function filterQuery(): void { diff --git a/src/Utils/Helper.php b/src/Utils/Helper.php deleted file mode 100644 index 42076b9..0000000 --- a/src/Utils/Helper.php +++ /dev/null @@ -1,37 +0,0 @@ -db = new QueryBuilder(); - parent::setUp(); - } - - public function testSelectBuilder() - { - $this->db->select('id', 'name'); - $this->db->table('user'); - - $expected = "SELECT id, name FROM user WHERE 1"; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testBlankBuild() - { - $this->db->from('post'); - - $expected = 'SELECT * FROM post WHERE 1'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testSelfJoinBuild() - { - $this->db->select('post.id', 'post.title', 'user.name AS authorName') - ->table('post') - ->selfJoin('user', 'user.id = post.user'); - - $expected = "SELECT post.id, post.title, user.name AS authorName FROM post, user WHERE user.id = post.user"; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testInnerJoinBuild() - { - $this->db->select('post.id, post.title', 'user.name as authorName') - ->from('post') - ->innerJoin('user', 'user.id = post.user'); - - $expected = "SELECT post.id, post.title, user.name as authorName FROM post INNER JOIN user ON user.id = post.user WHERE 1"; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testLeftJoinBuild() - { - $this->db->select('post.id', 'post.title', 'user.name as authorName'); - $this->db->from('post'); - $this->db->leftJoin('user', 'user.id=post.user'); - - $expected = "SELECT post.id, post.title, user.name as authorName FROM post LEFT JOIN user ON user.id=post.user WHERE 1"; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testRightJoinBuild() - { - $this->db->select('post.id', 'post.title', 'user.name as authorName'); - $this->db->from('post'); - $this->db->rightJoin('user', 'user.id=post.user'); - - $expected = "SELECT post.id, post.title, user.name as authorName FROM post RIGHT JOIN user ON user.id=post.user WHERE 1"; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testLeftOuterJoinBuild() - { - $this->db->select('post.id', 'post.title', 'user.name as authorName'); - $this->db->from('post'); - $this->db->leftOuterJoin('user', 'user.id=post.user'); - - $expected = "SELECT post.id, post.title, user.name as authorName FROM post LEFT OUTER JOIN user ON user.id=post.user WHERE 1"; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testRightOuterJoinBuild() - { - $this->db->select('post.id', 'post.title', 'user.name as authorName'); - $this->db->from('post'); - $this->db->rightOuterJoin('user', 'user.id=post.user'); - - $expected = "SELECT post.id, post.title, user.name as authorName FROM post RIGHT OUTER JOIN user ON user.id=post.user WHERE 1"; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testLimitStatement() - { - $this->db->select('id') - ->from('book') - ->limit(5); - - $expected = 'SELECT id FROM book WHERE 1 LIMIT 5'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testOffsetStatement() - { - $this->db->select('id') - ->from('book') - ->offset(5); - - $expected = 'SELECT id FROM book WHERE 1 OFFSET 5'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testOffsetLimitStatement() - { - $this->db->select('id') - ->from('book') - ->offset(50) - ->limit(25); - - $expected = 'SELECT id FROM book WHERE 1 LIMIT 50, 25'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testNegativeOffsetLimitStatement() - { - $this->db->select('id') - ->from('book') - ->offset(-25) - ->limit(-20); - - // If limit and offset are negative integers, their absolute values are taken. - $expected = 'SELECT id FROM book WHERE 1 LIMIT 25, 20'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testSelectDistinctStatement() - { - $this->db->selectDistinct('name') - ->from('book'); - $expected = 'SELECT DISTINCT(name) FROM book WHERE 1'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testSelectDistinctJoinStatement() - { - $this->db->selectDistinct('author.name') - ->from('book') - ->innerJoin('author', 'author.id=book.author'); - $expected = 'SELECT DISTINCT(author.name) FROM book INNER JOIN author ON author.id=book.author WHERE 1'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testOrderByStatement() - { - $this->db->select('name') - ->from('book') - ->orderBy('authorId', 'ASC') - ->orderBy('id', 'DESC') - ->limit(10); - - $expected = 'SELECT name FROM book WHERE 1 ORDER BY authorId ASC, id DESC LIMIT 10'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testInsertStatementBuild() - { - $this->db->from('post'); - - $data = [ - 'title' => 'Post Title', - 'content' => 'Post Content', - 'author' => 5, - 'status' => true, - ]; - $this->db->set($data); - - - $expected = 'INSERT INTO post (title, content, author, status) VALUES (:title, :content, 5, :status);'; - $this->assertEquals($expected, $this->db->generateInsertQuery()); - $this->db->resetStructure(); - } - - public function testInsertBatchStatementBuild() - { - - $this->db->from('post'); - - $this->db->set([ - 'title' => 'Post Title #1', - 'content' => 'Post Content #1', - 'author' => 5, - 'status' => true, - ]) - ->set([ - 'title' => 'Post Title #2', - 'content' => 'Post Content #2', - 'status' => false, - ]); - - $expected = 'INSERT INTO post (title, content, author, status) VALUES (:title, :content, 5, :status), (:title_1, :content_1, NULL, :status_1);'; - $this->assertEquals($expected, $this->db->generateBatchInsertQuery()); - $this->db->resetStructure(); - } - - public function testUpdateStatementBuild() - { - - $this->db->from('post') - ->where('status', '=', true) - ->limit(5); - - $data = [ - 'title' => 'New Title', - 'status' => false, - ]; - $this->db->set($data); - - $expected = 'UPDATE post SET title = :title, status = :status_1 WHERE status = :status LIMIT 5'; - - $this->assertEquals($expected, $this->db->generateUpdateQuery()); - $this->db->resetStructure(); - } - - public function testUpdateBatchStatementBuild() - { - - $this->db->from('post') - ->where('status', '=', true); - - $this->db->set([ - 'id' => 5, - 'title' => 'New Title #5', - 'content' => 'New Content #5', - ])->set([ - 'id' => 10, - 'title' => 'New Title #10', - ]); - - $expected = 'UPDATE post SET title = CASE WHEN id = 5 THEN :title WHEN id = 10 THEN :title_1 ELSE title END, content = CASE WHEN id = 5 THEN :content ELSE content END WHERE status = :status AND id IN (5, 10)'; - - $this->assertEquals($expected, $this->db->generateUpdateBatchQuery('id')); - $this->db->resetStructure(); - } - - public function testDeleteStatementBuild() - { - - $this->db->from('post') - ->where('authorId', '=', 5) - ->limit(100); - - $expected = 'DELETE FROM post WHERE authorId = 5 LIMIT 100'; - - $this->assertEquals($expected, $this->db->generateDeleteQuery()); - $this->db->resetStructure(); - } - - public function testWhereSQLFunctionStatementBuild() - { - $this->db->from('post') - ->andBetween('date', ['2022-05-07', 'CURDATE()']); - - $expected = 'SELECT * FROM post WHERE date BETWEEN :date AND CURDATE()'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testWhereRegexpSQLStatementBuild() - { - $this->db->from('post') - ->regexp('title', '^M[a-z]K$'); - - $expected = 'SELECT * FROM post WHERE title REGEXP :title'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testSelectCoalesceSQLStatementBuild() - { - $this->db->select('post.title') - ->selectCoalesce('stat.view', 0) - ->from('post') - ->leftJoin('stat', 'stat.id=post.id') - ->where('post.id', 5); - - $expected = 'SELECT post.title, COALESCE(stat.view, 0) FROM post LEFT JOIN stat ON stat.id=post.id WHERE post.id = 5'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testSelectCoalesceDefaultValue() - { - $this->db->select('post.title') - ->selectCoalesce('stat.view', 'post.view', 'views') - ->from('post') - ->leftJoin('stat', 'stat.id=post.id') - ->where('post.id', 5); - - $expected = 'SELECT post.title, COALESCE(stat.view, post.view) AS views FROM post LEFT JOIN stat ON stat.id=post.id WHERE post.id = 5'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - - public function testTableAliasSQLStatementBuild() - { - $this->db->select('p.title') - ->select('s.view as s_view') - ->from('post as p') - ->leftJoin('stat as s', 's.id=p.id') - ->where('p.id', 5); - - $expected = 'SELECT p.title, s.view as s_view FROM post as p LEFT JOIN stat as s ON s.id=p.id WHERE p.id = 5'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testTableJoinAliasSQLStatementBuild() - { - $this->db->select('p.title') - ->select('s.view as s_view') - ->from('post p') - ->leftJoin('stat s', 's.id=p.id') - ->where('p.id', 5); - - $expected = 'SELECT p.title, s.view as s_view FROM post p LEFT JOIN stat s ON s.id=p.id WHERE p.id = 5'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testWhereGroupStatement() - { - $this->db->select('id') - ->from('users') - ->where('status', 1) - ->group(function (QueryBuilder $builder) { - $builder->where('type', 3) - ->where('type', 4); - }); - - $expected = 'SELECT id FROM users WHERE status = 1 AND (type = 3 AND type = 4)'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testWhereGroupMultipleStatement() - { - $this->db->select('id, title, content, url') - ->from('posts') - ->where('status', 1) - ->group(function (QueryBuilder $db) { - $db->where('user_id', 1) - ->where('datetime', '>=', date("Y-m-d")); - }, 'or') - ->group(function (QueryBuilder $db) { - $db->group(function (QueryBuilder $db) { - $db->where('id', 2) - ->where('status', 3); - }, 'or') - ->group(function (QueryBuilder $db) { - $db->where('id', 4) - ->where('status', 5); - }, 'or'); - }, 'or'); - - $expected = 'SELECT id, title, content, url FROM posts WHERE status = 1 AND (user_id = 1 AND datetime >= :datetime) OR ((id = 2 AND status = 3) OR (id = 4 AND status = 5))'; - - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - - public function testJoinClosureGive() - { - - $this->db->select('u.id', 'u.name', 'u.status, p.title') - ->from('users AS u') - ->where('u.status', 1) - ->join('posts AS p', function (QueryBuilder $builder) { - $builder->on('p.user_id', 'u.id') - ->where('p.publisher_time', '>=', $builder->raw('NOW()')); - }) - ->join('categories AS c', function (QueryBuilder $builder) { - $builder->on('c.id', 'p.category_id') - ->on('c.blog_id', 'u.blog_id') - ->where('c.status', 1) - ->having($builder->raw('COUNT(p.category_id) > 1')); - })->limit(5); - - $expected = 'SELECT u.id, u.name, u.status, p.title FROM users AS u INNER JOIN posts AS p ON p.user_id = u.id INNER JOIN categories AS c ON c.id = p.category_id AND c.blog_id = u.blog_id WHERE u.status = 1 AND p.publisher_time >= NOW() AND c.status = 1 HAVING COUNT(p.category_id) > 1 LIMIT 5'; - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testSubQuery() - { - - $this->db->select('u.name') - ->from('users AS u') - ->whereIn('u.id', $this->db->subQuery(function (QueryBuilder $builder) { - $builder->select('id') - ->from('roles') - ->where('name', 'admin'); - })); - $expected = 'SELECT u.name FROM users AS u WHERE u.id IN (SELECT id FROM roles WHERE name = :name)'; - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - - public function testSubQueryJoinTable() - { - $this->db->select('u.name, p.title') - ->from('users AS u') - ->join($this->db->subQuery(function (QueryBuilder $builder) { - $builder->select('id, title, user_id') - ->from('posts') - ->where('user_id', 5); - }, 'p'), 'p.user_id = u.id', ''); - - $expected = 'SELECT u.name, p.title FROM users AS u JOIN (SELECT id, title, user_id FROM posts WHERE user_id = 5) AS p ON p.user_id = u.id WHERE 1'; - $this->assertEquals($expected, $this->db->generateSelectQuery()); - $this->db->resetStructure(); - } - -}