[fetch|fetchAll]
+ * @return array|EntityInterface|EntityInterface[]|object|object[]|null + */ + private function dbGetFetchMode(string $pdoMethod) + { + if(!isset($this->_DBLastStatement)){ + return null; + } + if(isset($this->_DBAsReturnType)){ + $asType = $this->_DBAsReturnType; + unset($this->_DBAsReturnType); + return $this->_DBLastStatement->{$pdoMethod}($asType); + } + if(!isset($this->entity)){ + return $this->_DBLastStatement->{$pdoMethod}(); + } + if(is_object($this->entity)){ + return $this->_DBLastStatement->{$pdoMethod}(\PDO::FETCH_INTO, $this->entity); + } + if(is_string($this->entity)){ + return $this->_DBLastStatement->{$pdoMethod}(\PDO::FETCH_CLASS, $this->entity); + } + return $this->_DBLastStatement->{$pdoMethod}(\PDO::FETCH_BOTH); + } + +} diff --git a/src/Entity.php b/src/Entity.php new file mode 100644 index 0000000..6067672 --- /dev/null +++ b/src/Entity.php @@ -0,0 +1,148 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database; + +use InitPHP\Database\Interfaces\EntityInterface; + +use function substr; +use function method_exists; +use function explode; +use function ucfirst; +use function array_search; +use function preg_split; +use function strtolower; +use function lcfirst; + +class Entity implements EntityInterface +{ + + protected array $DBAttributes = []; + + protected array $DBCamelCaseAttributeNames = []; + + protected array $DBOriginalAttributes = []; + + public function __construct(?array $data = null) + { + $this->setUp($data); + } + + public function __call($name, $arguments) + { + if(substr($name, -9) !== 'Attribute'){ + throw new \RuntimeException('There is no ' . $name . ' method.'); + } + $startWith = substr($name, 0, 3); + if($startWith === 'get' && empty($arguments)){ + $key = $this->camelCaseAttributeNameNormalize(substr($name, 3, -9)); + return $this->DBAttributes[$key] ?? null; + } + if($startWith === 'set'){ + $key = $this->camelCaseAttributeNameNormalize(substr($name, 3, -9)); + return $this->DBAttributes[$key] = $arguments[0]; + } + throw new \RuntimeException('There is no ' . $name . ' method.'); + } + + public function __set($key, $value) + { + $attrName = $this->attributeName($key); + $method = 'set'.$attrName.'Attribute'; + return $this->DBAttributes[$key] = method_exists($this, $method) ? $this->{$method}($value) : $value; + } + + public function __get($key) + { + $attrName = $this->attributeName($key); + $method = 'get'.$attrName.'Attribute'; + $value = $this->DBAttributes[$key] ?? ($this->DBAttributes[$attrName] ?? null); + if(method_exists($this, $method)){ + return $this->{$method}($value); + } + return $value; + } + + public function __isset($key) + { + return isset($this->DBAttributes[$key]); + } + + public function __unset($key) + { + if(isset($this->DBAttributes[$key])){ + unset($this->DBAttributes[$key]); + } + } + + public function __debugInfo() + { + return $this->DBAttributes; + } + + public final function getAttributes(): array + { + return $this->DBAttributes; + } + + protected function setUp(?array $data = null): void + { + $this->syncOriginal(); + $this->fill($data); + } + + protected function fill(?array $data = null): self + { + if($data !== null){ + foreach ($data as $key => $value) { + $this->__set($key, $value); + } + } + return $this; + } + + protected function syncOriginal(): self + { + $this->DBOriginalAttributes = $this->DBAttributes; + return $this; + } + + private function attributeName(string $key): string + { + if(isset($this->DBCamelCaseAttributeNames[$key])){ + return $this->DBCamelCaseAttributeNames[$key]; + } + $attrName = ''; + $parse = explode('_', $key); + foreach ($parse as $col) { + $attrName .= ucfirst($col); + } + return $this->DBCamelCaseAttributeNames[$key] = $attrName; + } + + private function camelCaseAttributeNameNormalize(string $name): string + { + if(($key = array_search($name, $this->DBCamelCaseAttributeNames, true)) !== FALSE){ + return $key; + } + $parse = preg_split('/(?=[A-Z])/', $name, -1, \PREG_SPLIT_NO_EMPTY); + $key = ''; + foreach ($parse as $value) { + $key .= '_' . strtolower($value); + } + return isset($this->DBAttributes[$key]) ? $key : lcfirst($name); + } + +} diff --git a/src/Exception/ConnectionException.php b/src/Exception/ConnectionException.php new file mode 100644 index 0000000..f72d9ac --- /dev/null +++ b/src/Exception/ConnectionException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Exception; + +class ConnectionException extends \Exception +{ + +} diff --git a/src/Exception/DatabaseException.php b/src/Exception/DatabaseException.php new file mode 100644 index 0000000..9ce56f7 --- /dev/null +++ b/src/Exception/DatabaseException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Exception; + +class DatabaseException extends \Exception +{ + +} diff --git a/src/Exception/ModelException.php b/src/Exception/ModelException.php new file mode 100644 index 0000000..6ea0dda --- /dev/null +++ b/src/Exception/ModelException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Exception; + +class ModelException extends \Exception +{ + +} diff --git a/src/Exception/ModelPermissionException.php b/src/Exception/ModelPermissionException.php new file mode 100644 index 0000000..84ba945 --- /dev/null +++ b/src/Exception/ModelPermissionException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Exception; + +class ModelPermissionException extends \Exception +{ + +} diff --git a/src/Exception/QueryExecuteException.php b/src/Exception/QueryExecuteException.php new file mode 100644 index 0000000..d5b927c --- /dev/null +++ b/src/Exception/QueryExecuteException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Exception; + +class QueryExecuteException extends \Exception +{ + +} diff --git a/src/Interfaces/ConnectionInterface.php b/src/Interfaces/ConnectionInterface.php new file mode 100644 index 0000000..6cebbfa --- /dev/null +++ b/src/Interfaces/ConnectionInterface.php @@ -0,0 +1,100 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Interfaces; + +use \InitPHP\Database\Exception\ConnectionException; + +interface ConnectionInterface +{ + + /** + * @used-by ConnectionInterface::getPDO() + * @return $this + * @throws ConnectionException + */ + public function connection(): self; + + /** + * @return void + */ + public function asConnectionGlobal(); + + /** + * @return void + */ + public function disconnection(); + + /** + * @return \PDO + */ + public function getPDO(): \PDO; + + /** + * @return string + */ + public function getDSN(): string; + + /** + * @param string $DSN + * @return $this + */ + public function setDSN(string $DSN): self; + + /** + * @return string + */ + public function getUsername(): string; + + /** + * @param string $username + * @return $this + */ + public function setUsername(string $username): self; + + /** + * @return string + */ + public function getPassword(): string; + + /** + * @param string $password + * @return $this + */ + public function setPassword(string $password): self; + + /** + * @return string + */ + public function getCharset(): string; + + /** + * @param string $charset + * @return $this + */ + public function setCharset(string $charset = 'utf8mb4'): self; + + /** + * @return string + */ + public function getCollation(): string; + + /** + * @param string $collation + * @return $this + */ + public function setCollation(string $collation = 'utf8mb4_general_ci'): self; + +} diff --git a/src/Interfaces/DBInterface.php b/src/Interfaces/DBInterface.php new file mode 100644 index 0000000..9971bbd --- /dev/null +++ b/src/Interfaces/DBInterface.php @@ -0,0 +1,173 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Interfaces; + +use InitPHP\Database\Exception\QueryExecuteException; + +interface DBInterface extends QueryBuilderInterface +{ + + /** + * DB::get() ya da DB::query() yöntemlerinin son işleminden etkilenen satır sayısını verir. + * + * @param null|DBInterface|\PDOStatement $dbOrPDOStatement + * @return int + */ + public function numRows($dbOrPDOStatement = null): int; + + /** + * DB::query() ya da DB::exec() ile yürütülmüş son SQL cümlesini verir. + * + * @return string|null + */ + public function lastSQL(): ?string; + + /** + * Son DBInterface::query() ile yürütülmüş son SQL cümlesinden varsa son eklenen PRIMARY KEY değeri. + * + * @return int|null + */ + public function insertId(): ?int; + + /** + * Kendisinden sonraki ilk sonuç için dönüş tipini \PDO::FETCH_ASSOC olarak değiştirir. + * + * @return DBInterface + */ + public function asAssoc(): DBInterface; + + /** + * Kendisinden sonraki ilk sonuç için dönüş tipini \PDO::FETCH_BOTH olarak değiştirir. + * + * @return DBInterface + */ + public function asArray(): DBInterface; + + /** + * Kendisinden sonraki ilk sonuç için dönüş tipini \PDO::FETCH_OBJ olarak değiştirir. + * + * @return DBInterface + */ + public function asObject(): DBInterface; + + /** + * Transaction işlemlerinin son durumunu verir. + * + * @return boolTransaction sırasında bir hata oluştuysa FALSE, herşey yolunda ise TRUE verir.
+ */ + public function transactionStatus(): bool; + + /** + * \PDO üzerinde Transaction sürecini başlatır. + * + * @param bool $testMode$testMode aktif ise (TRUE) işlemler her durumda (DB::transactionComplete() tarafından) geri alınır.
+ * @return DBInterface + */ + public function transactionStart(bool $testMode = false): DBInterface; + + /** + * Bir PDO transaction sürecini sona erdirir. + * + * İşlemlerden en az biri başarısız olursa ya da $testMode açıksa süreç sırasındaki işlemler geri alınır. + * + * @return DBInterface + */ + public function transactionComplete(): DBInterface; + + /** + * DB::get() ile yürütülmüş sorgudan etkilenen son satırı döndürür. + * + * Bu \PDOStatement::fetchAll() eş değeridir. + * + * @return object[]|EntityInterface[]|array|null + */ + public function rows(); + + /** + * DB::get() ile yürütülmüş sorgudan etkilenen son satırı döndürür. + * + * Bu \PDOStatement::fetch() eş değeridir. + * + * @return object|EntityInterface|array|null + */ + public function row(); + + /** + * DB::get() ile yürütülmüş \PDOStatement için \PDOStatement::fetchColumn() sonucunu verir. + * + * @link https://www.php.net/manual/tr/pdostatement.fetchcolumn.php + * @param int $column + * @return mixed + */ + public function column(int $column = 0); + + /** + * Varsa SQL cümlesi içindeki parametreleri tanımlar. + * + * Belirtilen parametreler DB::get() yöntemi ile sorgu yürütülürken \PDO::execute() işlevine aktarılır. + * + * @param array $arguments + * @return DBInterface + */ + public function setParams(array $arguments): DBInterface; + + /** + * QueryBuilder ile kurulmuş SQL cümlesini kurar, yürütür ve sınıfa yükler. + * + * @param string|null $tableBelirtilirse; QueryBuilder::from() işlevine gönderilir.
+ * @return \PDOStatement|false + */ + public function get(?string $table = null); + + /** + * Farklı bir \PDOStatement nesneli DB sınıfının örneğini verir. + * + * @param DBInterface|\PDOStatement $dbOrPDOStatement + * @return DBInterface + * @throws \InvalidArgumentException$dbOrPDOStatement parametresi farklı bir veri tipinde ise.
+ */ + public function fromGet($dbOrPDOStatement): DBInterface; + + /** + * QueryBuilder sıfırlanmadan SQL cümlesinin kurar, yürütür ve etkilelen satır sayısını döndürür. + * + * @return int + */ + public function count(): int; + + /** + * Bir SQL sorgu cümlesini yürütür ve sonucu \PDOStatement nesnesi olarak döndürür. + * + * @used-by DBInterface::get() + * @param string $sqlSQL Statement
+ * @param array|null $parametersVarsa, PDO::execute() yöntemine gönderilecek parametre dizisi.
+ * @return \PDOStatement|false + * @throws QueryExecuteExceptionSQL sorgusu yürütülürken \PDOException istisnası fırlatırlırsa.
+ */ + public function query(string $sql, ?array $parameters = null); + + + /** + * SQL sorgu cümlesini yürütür ve etkilenen satır sayısını döndürür. + * + * @used-by DBInterface::count() + * @param string $sqlSQL Statement
+ * @return int + * @throws QueryExecuteExceptionSQL sorgusu yürütülürken \PDOException istisnası fırlatırlırsa.
+ */ + public function exec(string $sql): int; + +} \ No newline at end of file diff --git a/src/Interfaces/EntityInterface.php b/src/Interfaces/EntityInterface.php new file mode 100644 index 0000000..1ec7e4c --- /dev/null +++ b/src/Interfaces/EntityInterface.php @@ -0,0 +1,27 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Interfaces; + +interface EntityInterface +{ + /** + * Returns the held data as an associative array. + * + * @return array + */ + public function getAttributes(): array; + +} diff --git a/src/Interfaces/ModelInterface.php b/src/Interfaces/ModelInterface.php new file mode 100644 index 0000000..37a8ee5 --- /dev/null +++ b/src/Interfaces/ModelInterface.php @@ -0,0 +1,191 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Interfaces; + +use InitPHP\Database\Exception\ModelPermissionException; + +interface ModelInterface extends DBInterface +{ + + /** + * Doğrulama vs. hata var mı? + * + * Oluşan hata varsa bunlar ModelInterface::getError() yöntemi ile alınabilir. + * + * @return bool + */ + public function isError(): bool; + + /** + * Doğrulama vs. hatalarını tutan hata dizisini verir. + * + * @return array + */ + public function getError(): array; + + /** + * Bir ya da daha fazla satırı ekler. ModelInterface::insert() yönteminin diğer adıdır. + * + * @uses ModelInterface::insert() + * @param array $data + * @return array|false + */ + public function create(array $data); + + /** + * Bir Entity nesnesini kullanarak veriyi ekler ya da günceller. + * + * @uses ModelInterface::update() + * @uses ModelInterface::insert() + * @param EntityInterface $entity + * @return array|false + */ + public function save(EntityInterface $entity); + + /** + * Bir ya da daha fazla satırı ekler. + * + * @used-by ModelInterface::save() + * @used-by ModelInterface::create() + * @param array $data + * @return array|false + * @throws ModelPermissionExceptionModel'in veri okuma izni yoksa.
+ */ + public function insert(array $data); + + /** + * Bir ya da daha fazla satırda güncelleme yapar. + * + * @param array $data + * @param null|int|string $id + * @return array|false + * @throws ModelPermissionExceptionModel'in veri güncelleme izni yoksa.
+ */ + public function update(array $data, $id = null); + + /** + * Bir ya da daha fazla veriyi siler. + * + * Eğer yumuşak silme kullanılmıyorsa; verileri kalıcı olarak sileceğini unutmayın. + * + * @param null|int|string $idVarsa PRIMARY KEY sütunun değeri
+ * @return array|false + * @throws ModelPermissionExceptionModel'in silme izni yoksa.
+ */ + public function delete($id = null); + + /** + * İlk satırı döndürür. + * + * @return EntityInterface|object|array|false + * @throws ModelPermissionExceptionModel'in veri okuma izni yoksa.
+ */ + public function first(); + + /** + * Bir veriyi arar ve döndürür. + * + * @param null|int|string $idVarsa PRIMARY KEY sütununun değeri.
+ * @return EntityInterface|object|array|false + * @throws ModelPermissionExceptionModel'in veri okuma izni yoksa.
+ */ + public function find($id = null); + + /** + * Sadece belli bir sütunu seçer ve sonucu döndürür. Bu Select ile sütun seçer. + * + * @param string $column + * @return EntityInterface[]|object[]|array|false + * @throws ModelPermissionExceptionModel'in veri okuma izni yoksa.
+ */ + public function findColumn(string $column); + + /** + * Belli aralıktaki verileri çeker ve döndürür. + * + * @param int $limit + * @param int $offset + * @return EntityInterface[]|object[]|array|false + * @throws ModelPermissionExceptionModel'in veri okuma izni yoksa.
+ */ + public function findAll(int $limit = 100, int $offset = 0); + + /** + * Tüm satırları çeker ve döndürür. + * + * @return EntityInterface[]|object[]|array|false + * @throws ModelPermissionExceptionModel'in veri okuma izni yoksa.
+ */ + public function all(); + + /** + * Sadece yumuşak silme ile silinmiş verileri seçer. + * + * @return ModelInterface + */ + public function onlyDeleted(): ModelInterface; + + /** + * Sadece yumuşak silme ile silinmemiş verileri seçer. + * + * @return ModelInterface + */ + public function onlyUndeleted(): ModelInterface; + + /** + * Yumuşak silme ile silinmiş verileri seçer ve kalıcı olarak siler. + * + * @return bool + */ + public function purgeDeleted(): bool; + + /** + * Model'in yeni veri oluşturma yetkisi var mı? + * + * @used-by ModelInterface::insert() + * @return bool + */ + public function isWritable(): bool; + + /** + * Model'in veri okumaya yetkisi var mı? + * + * @used-by ModelInterface::first() + * @used-by ModelInterface::find() + * @used-by ModelInterface::findAll() + * @used-by ModelInterface::findColumn() + * @used-by ModelInterface::all() + * @return bool + */ + public function isReadable(): bool; + + /** + * Model'in günceleme yetkisi var mı? + * + * @used-by ModelInterface::update() + * @return bool + */ + public function isUpdatable(): bool; + + /** + * Model veri silme yetkisi var mı? + * + * @used-by ModelInterface::delete() + * @return bool + */ + public function isDeletable(): bool; + +} diff --git a/src/Interfaces/QueryBuilderInterface.php b/src/Interfaces/QueryBuilderInterface.php new file mode 100644 index 0000000..77e64e1 --- /dev/null +++ b/src/Interfaces/QueryBuilderInterface.php @@ -0,0 +1,662 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database\Interfaces; + +interface QueryBuilderInterface +{ + + /** + * Resets QueryBuilder properties except temporary SQL statement memory. + * + * @uses QueryBuilderInterface::clear() + * @return QueryBuilderInterface + */ + public function reset(): QueryBuilderInterface; + + /** + * Resets all QueryBuilder properties. + * + * @used-by QueryBuilderInterface::reset() + * @return QueryBuilderInterface + */ + public function clear(): QueryBuilderInterface; + + /** + * ... UNION ... + * + * @return QueryBuilderInterface + */ + public function union(): QueryBuilderInterface; + + /** + * ... UNION ALL ... + * + * @return QueryBuilderInterface + */ + public function unionAll(): QueryBuilderInterface; + + /** + * SELECT id, title, ...$columns + * + * @param string ...$columns + * @return QueryBuilderInterface + */ + public function select(string ...$columns): QueryBuilderInterface; + + /** + * SELECT COUNT($column) + * + * @param string $column + * @return QueryBuilderInterface + */ + public function selectCount(string $column): QueryBuilderInterface; + + /** + * SELECT $column AS $alias + * + * @param string $column + * @param string $alias + * @return QueryBuilderInterface + */ + public function selectAs(string $column, string $alias): QueryBuilderInterface; + + /** + * FROM $table + * + * @param string $table + * @return QueryBuilderInterface + */ + public function from(string $table): QueryBuilderInterface; + + /** + * @param string $table + * @param string $onStmtExample : "post.author=user.id"
+ * @param string $type + * @return QueryBuilderInterface + * @throws \InvalidArgumentException+ * $type is not supported or $onStmt is not in the correct format. + *
+ */ + public function join(string $table, string $onStmt, string $type = 'INNER'): QueryBuilderInterface; + + /** + * FROM post, user WHERE post.author = user.id + * + * @param string $tableThe table name to include.
+ * @param string $onStmtExample : "post.author=user.id"
+ * @return QueryBuilderInterface + * @throws \InvalidArgumentException$onStmt is not in the correct format.
+ */ + public function selfJoin(string $table, string $onStmt): QueryBuilderInterface; + + /** + * INNER JOIN user ON post.author = user.id + * + * @param string $tableThe name of the table to join.
+ * @param string $onStmtExample : "post.author=user.id"
+ * @return QueryBuilderInterface + * @throws \InvalidArgumentException$onStmt is not in the correct format.
+ */ + public function innerJoin(string $table, string $onStmt): QueryBuilderInterface; + + /** + * LEFT JOIN user ON post.author = user.id + * + * @param string $tableThe name of the table to join.
+ * @param string $onStmtExample : "post.author=user.id"
+ * @return QueryBuilderInterface + * @throws \InvalidArgumentException$onStmt is not in the correct format.
+ */ + public function leftJoin(string $table, string $onStmt): QueryBuilderInterface; + + /** + * RIGHT JOIN user ON post.author = user.id + * + * @param string $tableThe name of the table to join.
+ * @param string $onStmtExample : "post.author=user.id"
+ * @return QueryBuilderInterface + * @throws \InvalidArgumentException$onStmt is not in the correct format.
+ */ + public function rightJoin(string $table, string $onStmt): QueryBuilderInterface; + + /** + * LEFT OUTER JOIN user ON post.author = user.id + * + * @param string $tableThe name of the table to join.
+ * @param string $onStmtExample : "post.author=user.id"
+ * @return QueryBuilderInterface + * @throws \InvalidArgumentException$onStmt is not in the correct format.
+ */ + public function leftOuterJoin(string $table, string $onStmt): QueryBuilderInterface; + + /** + * RIGHT OUTER JOIN user ON post.author = user.id + * + * @param string $tableThe name of the table to join.
+ * @param string $onStmtExample : "post.author=user.id"
+ * @return QueryBuilderInterface + * @throws \InvalidArgumentException$onStmt is not in the correct format.
+ */ + public function rightOuterJoin(string $table, string $onStmt): QueryBuilderInterface; + + /** + * It is used to group where clauses. + * + * @param \Closure $groupQueryBuilderInterface is passed as a parameter to this callback function.
+ * @return QueryBuilderInterface + */ + public function group(\Closure $group): QueryBuilderInterface; + + /** + * Adds a SQL Where clause. + * + * @param string $column + * @param mixed $value + * @param string $mark + * @param string $logical + * @return QueryBuilderInterface + */ + public function where(string $column, $value, string $mark = '=', string $logical = 'AND'): QueryBuilderInterface; + + /** + * Injects a string into the Where SQL clause. + * + * @param string $statement + * @return QueryBuilderInterface + */ + public function andWhereInject(string $statement): QueryBuilderInterface; + + /** + * Injects a string into the Where SQL clause. + * + * @param string $statement + * @return QueryBuilderInterface + */ + public function orWhereInject(string $statement): QueryBuilderInterface; + + /** + * Constructs a sentence to be combined with AND in a where clause. + * + * @param string $column + * @param mixed $value + * @param string $mark + * @return QueryBuilderInterface + */ + public function andWhere(string $column, $value, string $mark = '='): QueryBuilderInterface; + + /** + * Constructs a sentence to be combined with OR in a where clause. + * + * @param string $column + * @param mixed $value + * @param string $mark + * @return QueryBuilderInterface + */ + public function orWhere(string $column, $value, string $mark = '='): QueryBuilderInterface; + + /** + * Adds the having clause. + * + * @param string $column + * @param mixed $value + * @param string $mark + * @param string $logical + * @return QueryBuilderInterface + */ + public function having(string $column, $value, string $mark = '', string $logical = 'AND'): QueryBuilderInterface; + + /** + * Injects a string into the having clause. + * + * @param string $statement + * @return QueryBuilderInterface + */ + public function andHavingInject(string $statement): QueryBuilderInterface; + + /** + * Injects a string into the having clause. + * + * @param string $statement + * @return QueryBuilderInterface + */ + public function orHavingInject(string $statement): QueryBuilderInterface; + + /** + * Adds order by to the SQL statement. + * + * @param string $column + * @param string $soft[ASC|DESC]
+ * @return QueryBuilderInterface + * @throws \InvalidArgumentExceptionIf $soft is invalid.
+ */ + public function orderBy(string $column, string $soft = 'ASC'): QueryBuilderInterface; + + /** + * Adds Group By to the SQL statement. + * + * @param string $column + * @return QueryBuilderInterface + */ + public function groupBy(string $column): QueryBuilderInterface; + + /** + * It tells the SQL statement how many rows/data to skip. + * + * @param int $offset + * @return QueryBuilderInterface + */ + public function offset(int $offset = 0): QueryBuilderInterface; + + /** + * Defines the number of rows/data that will be affected by the SQL statement. + * + * @param int $limit + * @return QueryBuilderInterface + */ + public function limit(int $limit): QueryBuilderInterface; + + /** + * WHERE column BETWEEN values[0] AND values[1] + * + * @param string $column + * @param array $values + * @param string $logical + * @return QueryBuilderInterface + */ + public function between(string $column, array $values, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column BETWEEN values[0] AND values[1] + * + * @param string $column + * @param array $values + * @return QueryBuilderInterface + */ + public function orBetween(string $column, array $values): QueryBuilderInterface; + + /** + * WHERE column BETWEEN values[0] AND values[1] + * + * @param string $column + * @param array $values + * @return QueryBuilderInterface + */ + public function andBetween(string $column, array $values): QueryBuilderInterface; + + /** + * WHERE column NOT BETWEEN values[0] AND values[1] + * + * @param string $column + * @param array $values + * @param string $logical + * @return QueryBuilderInterface + */ + public function notBetween(string $column, array $values, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column NOT BETWEEN values[0] AND values[1] + * + * @param string $column + * @param array $values + * @return QueryBuilderInterface + */ + public function orNotBetween(string $column, array $values): QueryBuilderInterface; + + /** + * WHERE column NOT BETWEEN values[0] AND values[1] + * + * @param string $column + * @param array $values + * @return QueryBuilderInterface + */ + public function andNotBetween(string $column, array $values): QueryBuilderInterface; + + /** + * WHERE FIND_IN_SET(column, value) + * + * @param string $column + * @param string|string[]|int[] $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function findInSet(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE FIND_IN_SET(column, value) + * + * @param string $column + * @param string|string[]|int[] $value + * @return QueryBuilderInterface + */ + public function orFindInSet(string $column, $value): QueryBuilderInterface; + + /** + * WHERE FIND_IN_SET(column, value) + * + * @param string $column + * @param $value + * @return QueryBuilderInterface + */ + public function andFindInSet(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column IN (values[0], values[1], ...) + * + * @param string $column + * @param int[]|string[]|array $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function in(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column IN (values[0], values[1], ...) + * + * @param string $column + * @param int[]|string[]|string $value + * @return QueryBuilderInterface + */ + public function orIn(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column IN (values[0], values[1], ...) + * + * @param string $column + * @param int[]|string[]|string $value + * @return QueryBuilderInterface + */ + public function andIn(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column NOT IN (values[0], values[1], ...) + * + * @param string $column + * @param int[]|string[]|string $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function notIn(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column NOT IN (values[0], values[1], ...) + * + * @param string $column + * @param int[]|string[]|string $value + * @return QueryBuilderInterface + */ + public function orNotIn(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column NOT IN (values[0], values[1], ...) + * + * @param string $column + * @param int[]|string[]|string $value + * @return QueryBuilderInterface + */ + public function andNotIn(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column LIKE "%value%" + * + * @param string $column + * @param $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function like(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column LIKE "%value%" + * + * @param string $column + * @param $value + * @return QueryBuilderInterface + */ + public function orLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column LIKE "%value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function andLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column LIKE "%value" + * + * @param string $column + * @param null|bool|int|float|string $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function startLike(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column LIKE "%value" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function orStartLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column LIKE "%value" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function andStartLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column LIKE "value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @param string $logical [AND|OR] + * @return QueryBuilderInterface + */ + public function endLike(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column LIKE "value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function orEndLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column LIKE "value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function andEndLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "%value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function notLike(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "%value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function orNotLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "%value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function andNotLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "%value" + * + * @param string $column + * @param null|bool|int|float|string $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function startNotLike(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "%value" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function orStartNotLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "%value" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function andStartNotLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function endNotLike(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function orEndNotLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column NOT LIKE "value%" + * + * @param string $column + * @param null|bool|int|float|string $value + * @return QueryBuilderInterface + */ + public function andEndNotLike(string $column, $value): QueryBuilderInterface; + + /** + * WHERE SOUNDEX(column) LIKE CONCAT('%', TRIM(TRAILING '0' FROM SOUNDEX(value)), '%') + * + * @param string $column + * @param $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function soundex(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE SOUNDEX(column) LIKE CONCAT('%', TRIM(TRAILING '0' FROM SOUNDEX(value)), '%') + * + * @param string $column + * @param $value + * @return QueryBuilderInterface + */ + public function orSoundex(string $column, $value): QueryBuilderInterface; + + /** + * WHERE SOUNDEX(column) LIKE CONCAT('%', TRIM(TRAILING '0' FROM SOUNDEX(value)), '%') + * + * @param string $column + * @param $value + * @return QueryBuilderInterface + */ + public function andSoundex(string $column, $value): QueryBuilderInterface; + + /** + * WHERE column IS value + * + * @param string $column + * @param $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function is(string $column, $value, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column IS value + * + * @param string $column + * @param $value + * @return QueryBuilderInterface + */ + public function orIs(string $column, $value = null): QueryBuilderInterface; + + /** + * WHERE column IS value + * + * @param string $column + * @param $value + * @return QueryBuilderInterface + */ + public function andIs(string $column, $value = null): QueryBuilderInterface; + + /** + * WHERE column IS NOT value + * + * @param string $column + * @param $value + * @param string $logical + * @return QueryBuilderInterface + */ + public function isNot(string $column, $value = null, string $logical = 'AND'): QueryBuilderInterface; + + /** + * WHERE column IS NOT value + * + * @param string $column + * @param $value + * @return QueryBuilderInterface + */ + public function orIsNot(string $column, $value = null): QueryBuilderInterface; + + /** + * WHERE column IS NOT value + * @param string $column + * @param $value + * @return QueryBuilderInterface + */ + public function andIsNot(string $column, $value = null): QueryBuilderInterface; + +} diff --git a/src/Model.php b/src/Model.php new file mode 100644 index 0000000..ff06a95 --- /dev/null +++ b/src/Model.php @@ -0,0 +1,703 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database; + +use \InitPHP\Database\Exception\{ModelException, ModelPermissionException}; +use InitPHP\Database\Interfaces\{ConnectionInterface, ModelInterface, EntityInterface}; +use \InitPHP\Validation\Validation; + +use const COUNT_RECURSIVE; + +use function get_called_class; +use function explode; +use function strtolower; +use function end; +use function count; +use function date; +use function trim; +use function is_string; +use function strtr; +use function in_array; +use function array_search; +use function is_array; +use function method_exists; +use function call_user_func_array; +use function array_merge; + +class Model extends DB implements ModelInterface +{ + /** + * @var string[] + */ + protected array $connection; + + /** + * Dönüş için kullanılacak Entity sınıfı ya da nesnesi. + * + * @var EntityInterface|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 = []; + + private const VALIDATION_MSG_KEYS = [ + 'integer', 'float', 'numeric', 'string', + 'boolean', 'array', 'mail', 'mailHost', 'url', + 'urlHost', 'empty', 'required', 'min', 'max', + 'length', 'range', 'regex', 'date', 'dateFormat', + 'ip', 'ipv4', 'ipv6', 'repeat', 'equals', 'startWith', + 'endWith', 'in', 'notIn', 'alpha', 'alphaNum', + 'creditCard', 'only', 'strictOnly', 'contains', 'notContains', + 'is_unique', 'allowedFields' + ]; + + protected array $errors = []; + + private static Validation $_DBValidation; + + public function __construct() + { + if(!empty($this->getProperty('allowedField', null))){ + if(!empty($this->getProperty('createdField'))){ + $this->allowedFields[] = $this->getProperty('createdField'); + } + if(!empty($this->getProperty('updatedField'))){ + $this->allowedFields[] = $this->getProperty('updatedField'); + } + if(!empty($this->getProperty('deletedField'))){ + $this->allowedFields[] = $this->getProperty('deletedField'); + } + } + if(empty($this->getProperty('table'))){ + $modelClass = get_called_class(); + $modelClassSplit = explode('\\', $modelClass); + $this->table = strtolower(end($modelClassSplit)); + } + if($this->getProperty('useSoftDeletes', true) !== FALSE && empty($this->getProperty('deletedField'))){ + throw new ModelException('There must be a delete column to use soft delete.'); + } + if(!isset(self::$_DBValidation)){ + self::$_DBValidation = new Validation(); + } + $this->validationMsgMergeAndSet(); + parent::__construct($this->getProperty('connection')); + } + + /** + * @inheritDoc + */ + public final function isError(): bool + { + return !empty($this->errors); + } + + /** + * @inheritDoc + */ + public final function getError(): array + { + return $this->errors; + } + + /** + * @inheritDoc + */ + public final function create(array $data) + { + return $this->insert($data); + } + + /** + * @inheritDoc + */ + public final function save(EntityInterface $entity) + { + $data = $entity->getAttributes(); + $primaryKey = $this->getProperty('primaryKey'); + if(!empty($primaryKey) && isset($entity->{$primaryKey})){ + return $this->update($data, $entity->{$primaryKey}); + } + return $this->insert($data); + } + + /** + * @inheritDoc + */ + public final function insert(array $data) + { + if($this->isWritable() === FALSE){ + throw new ModelPermissionException('"' . get_called_class() . '" is not a writable model.'); + } + $data = $this->callbacksFunctionHandler($data, 'beforeInsert'); + if(count($data) !== count($data, COUNT_RECURSIVE)){ + $rows = $data; + $data = []; + foreach ($rows as $row) { + if(($row = $this->singleInsertDataProcess($row)) === FALSE){ + return false; + } + $data[] = $row; + } + unset($rows); + }else{ + if(($data = $this->singleInsertDataProcess($data)) === FALSE){ + return false; + } + } + $sql = $this->from($this->getProperty('table'))->insertStatementBuild($data); + $this->clear(); + if($this->query($sql) === FALSE){ + return false; + } + return $data = $this->callbacksFunctionHandler($data, 'afterInsert'); + } + + + /** + * @inheritDoc + */ + public final function update(array $data, $id = null) + { + if($this->isUpdatable() === FALSE){ + throw new ModelPermissionException('"' . get_called_class() . '" is not a updatable model.'); + } + $data = $this->callbacksFunctionHandler($data, 'beforeUpdate'); + $where = $id !== null && !empty($this->getProperty('primaryKey')) ? [$this->getProperty('primaryKey') => $id] : []; + foreach ($data as $key => $value) { + if($this->isValid($key, $value, $where) === FALSE){ + return false; + } + } + if(!empty($this->getProperty('updatedField'))) { + $data[$this->getProperty('updatedField')] = date('c'); + } + $sql = $this->from($this->getProperty('table'))->updateStatementBuild($data); + $this->clear(); + if($this->query($sql) === FALSE){ + return false; + } + return $data = $this->callbacksFunctionHandler($data, 'afterUpdate'); + } + + /** + * @inheritDoc + */ + public final function delete($id = null) + { + if($this->isDeletable() === FALSE){ + throw new ModelPermissionException('"' . get_called_class() . '" is not a deletable model.'); + } + $res = $this->from($this->table); + if($id !== null && !empty($this->getProperty('primaryKey'))){ + $res->where($this->getProperty('primaryKey'), $id, '='); + } + $clone = clone $res; + $res->asAssoc()->get(); + $data = $res->rows(); + $data = $this->callbacksFunctionHandler($data, 'beforeDelete'); + + if($this->getProperty('useSoftDeletes', true) !== FALSE){ + $sql = $this->updateStatementBuild([$this->getProperty('deletedField') => date('c')]); + }else{ + $sql = $this->deleteStatementBuild(); + } + $this->clear(); + if($this->query($sql) === FALSE){ + return false; + } + return $data = $this->callbacksFunctionHandler($data, 'afterDelete'); + } + + /** + * @inheritDoc + */ + public final function first() + { + if($this->isReadable() === FALSE){ + throw new ModelPermissionException('"' . get_called_class() . '" is not a readable model.'); + } + $res = $this->offset(0) + ->limit(1); + $res->get(); + $row = $res->row(); + return !empty($row) ? $row : false; + } + + /** + * @inheritDoc + */ + public final function find($id = null) + { + if($this->isReadable() === FALSE){ + throw new ModelPermissionException('"' . get_called_class() . '" is not a readable model.'); + } + $res = $this->offset(0)->limit(1); + if($id !== null && !empty($this->getProperty('primaryKey'))){ + $res->where($this->getProperty('primaryKey'), $id, '='); + } + $res->get(); + $row = $res->row(); + return !empty($row) ? $row : false; + } + + /** + * @inheritDoc + */ + public function findColumn(string $column) + { + if($this->isReadable() === FALSE){ + throw new ModelPermissionException('"' . get_called_class() . '" is not a readable model.'); + } + $res = $this->select($column); + $res->get(); + if($res->numRows() < 1){ + return false; + } + $row = $res->rows(); + return !empty($row) ? $row : false; + } + + /** + * @inheritDoc + */ + public function findAll(int $limit = 100, int $offset = 0) + { + if($this->isReadable() === FALSE){ + throw new ModelPermissionException('"' . get_called_class() . '" is not a readable model.'); + } + $res = $this->offset($offset) + ->limit($limit); + $res->get(); + if($res->numRows() < 1){ + return false; + } + $row = $res->rows(); + return !empty($row) ? $row : false; + } + + /** + * @inheritDoc + */ + public function all() + { + if($this->isReadable() === FALSE){ + throw new ModelPermissionException('"' . get_called_class() . '" is not a readable model.'); + } + $this->get(); + $row = $this->rows(); + return !empty($row) ? $row : false; + } + + /** + * @inheritDoc + */ + public function onlyDeleted(): self + { + if(!empty($this->getProperty('useSoftDeletes')) && !empty($this->getProperty('deletedField'))){ + $this->isNot($this->getProperty('deletedField'), null); + } + return $this; + } + + /** + * @inheritDoc + */ + public function onlyUndeleted(): self + { + if(!empty($this->getProperty('useSoftDeletes')) && !empty($this->getProperty('deletedField'))){ + $this->is($this->getProperty('deletedField'), null); + } + return $this; + } + + /** + * @inheritDoc + */ + public function purgeDeleted(): bool + { + if($this->isDeletable() === FALSE){ + return false; + } + if(!empty($this->getProperty('useSoftDeletes')) && !empty($this->getProperty('deletedField'))){ + $sql = $this->isNot($this->getProperty('deletedField'), null)->deleteStatementBuild(); + $this->clear(); + if($sql === ''){ + return false; + } + if($this->query($sql) === FALSE){ + return false; + } + return true; + } + return false; + } + + /** + * @inheritDoc + */ + public function isWritable(): bool + { + return $this->getProperty('writable', true); + } + + /** + * @inheritDoc + */ + public function isReadable(): bool + { + return $this->getProperty('readable', true); + } + + /** + * @inheritDoc + */ + public function isUpdatable(): bool + { + return $this->getProperty('updatable', true); + } + + /** + * @inheritDoc + */ + public function isDeletable(): bool + { + return $this->getProperty('deletable', true); + } + + protected final function setError(string $column, string $msg, array $context = []): void + { + $column = trim($column); + if(!isset($context['model'])){ + $context['model'] = get_called_class(); + } + $replace = []; $i = 0; + foreach ($context as $key => $value) { + if(!is_string($value)){ + $value = (string)$value; + } + $replace['{'.$key.'}'] = $value; + $replace['{'.$i.'}'] = $value; + ++$i; + } + $msg = strtr($msg, $replace); + if(!empty($column)){ + $this->errors[$column] = $msg; + return; + } + $this->errors[] = $msg; + } + + protected final function getProperty($property, $default = null) + { + return $this->{$property} ?? $default; + } + + private function singleInsertDataProcess($data) + { + $res = []; + foreach ($data as $key => $value) { + if(!empty($this->allowedFields) && in_array($key, $this->allowedFields, true) === FALSE){ + continue; + } + if($this->isValid($key, $value, []) === FALSE){ + return false; + } + $res[$key] = $value; + } + $data = $res; + unset($res); + if(empty($data)){ + return false; + } + if(!empty($this->createdField)){ + $data[$this->createdField] = date('c'); + } + return $data; + } + + private function isValid($column, $value, $uniqueWhere = []): bool + { + $methods = $this->columnValidationMethods($column); + if(empty($methods)){ + return true; + } + $localeArray = []; + foreach (self::VALIDATION_MSG_KEYS as $msgKey) { + $localeArray[$msgKey] = $this->validationMsg[$column][$msgKey] ?? $this->validationMsg[$msgKey]; + } + $validation = self::$_DBValidation + ->setLocaleArray($localeArray) + ->setData([$column => $value]); + if(in_array('is_unique', $methods)){ + $key = array_search('is_unique', $methods); + unset($methods[$key]); + $res = clone $this; + $res->clear() + ->select($column) + ->where($column, $value, '='); + if(is_array($uniqueWhere) && !empty($uniqueWhere)){ + foreach ($uniqueWhere as $uKey => $uVal) { + $res->where($uKey, $uVal, '!='); + } + } + $res->limit(1)->get(); + if($res->numRows() > 0){ + $this->setError($column, ($this->validationMsg[$column]['is_unique'] ?? '{field} must be unique.'), ['field' => $column]); + return false; + } + unset($res); + if(empty($methods)){ + return true; + } + } + foreach ($methods as $rule) { + $validation->rule($column, $rule); + } + if($validation->validation()){ + return true; + } + foreach ($validation->getError() as $err) { + $this->setError($column, $err); + } + return false; + } + + private final function callbacksFunctionHandler(array $data, string $method) + { + if($this->getProperty('allowedCallbacks', true) === FALSE){ + return $data; + } + if(empty($this->getProperty($method, null))){ + return $data; + } + $callbacks = $this->getProperty($method, null); + if(!is_array($callbacks)){ + return $data; + } + foreach ($callbacks as $callback) { + if(is_string($callback)){ + if(method_exists($this, $callback) === FALSE){ + continue; + } + $data = call_user_func_array([$this, $callback], [$data]); + continue; + } + if(!is_callable($callback)){ + continue; + } + $data = call_user_func_array($callback, [$data]); + } + return $data; + } + + private function columnValidationMethods(string $column): array + { + $methods = $this->validation[$column] ?? []; + return is_string($methods) ? explode('|', $methods) : $methods; + } + + private function validationMsgMergeAndSet() + { + $defaultMsg = [ + 'notValidDefault' => 'The {field} value is not valid.', + 'integer' => '{field} must be an integer.', + 'float' => '{field} must be an float.', + 'numeric' => '{field} must be an numeric.', + 'string' => '{field} must be an string.', + 'boolean' => '{field} must be an boolean', + 'array' => '{field} must be an Array.', + 'mail' => '{field} must be an E-Mail address.', + 'mailHost' => '{field} the email must be a {2} mail.', + 'url' => '{field} must be an URL address.', + 'urlHost' => 'The host of the {field} url must be {2}.', + 'empty' => '{field} must be empty.', + 'required' => '{field} cannot be left blank.', + 'min' => '{field} must be greater than or equal to {2}.', + 'max' => '{field} must be no more than {2}.', + 'length' => 'The {field} length range must be {2}.', + 'range' => 'The {field} range must be {2}.', + 'regex' => '{field} must match the {2} pattern.', + 'date' => '{field} must be a date.', + 'dateFormat' => '{field} must be a correct date format.', + 'ip' => '{field} must be the IP Address.', + 'ipv4' => '{field} must be the IPv4 Address.', + 'ipv6' => '{field} must be the IPv6 Address.', + 'repeat' => '{field} must be the same as {field1}', + 'equals' => '{field} can only be {2}.', + 'startWith' => '{field} must start with "{2}".', + 'endWith' => '{field} must end with "{2}".', + 'in' => '{field} must contain {2}.', + 'notIn' => '{field} must not contain {2}.', + 'alpha' => '{field} must contain only alpha characters.', + 'alphaNum' => '{field} can only be alphanumeric.', + 'creditCard' => '{field} must be a credit card number.', + 'only' => 'The {field} value is not valid.', + 'strictOnly' => 'The {field} value is not valid.', + 'contains' => '{field} must contain {2}.', + 'notContains' => '{field} must not contain {2}.', + 'is_unique' => '{field} must be unique.', + 'allowedFields' => 'Access is not granted to any of the specified tables.' + ]; + static::$_DBValidation->labels($this->getProperty('validationLabels', [])); + $this->validationMsg = array_merge($defaultMsg, $this->getProperty('validationMsg', [])); + } + +} diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php new file mode 100644 index 0000000..9b1bd2b --- /dev/null +++ b/src/QueryBuilder.php @@ -0,0 +1,1082 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Database; + +use const COUNT_RECURSIVE; + +use function trim; +use function in_array; +use function strtoupper; +use function str_replace; +use function preg_match; +use function call_user_func_array; +use function abs; +use function explode; +use function implode; +use function end; +use function count; +use function ucfirst; +use function property_exists; +use function is_array; +use function is_int; +use function is_numeric; +use function is_bool; +use function is_iterable; +use function strpos; +use function stripos; + +trait QueryBuilder +{ + + protected string $_QB_Prefix = ''; + + protected static string $_QB_StaticPrefix; + + private array $_supported_join_types = [ + 'INNER', 'LEFT', 'RIGHT', 'LEFT OUTER', 'RIGHT OUTER', 'SELF' + ]; + + protected array $QB_Select = []; + protected array $QB_From = []; + protected array $QB_Where = []; + protected array $QB_Having = []; + protected array $QB_OrderBy = []; + protected array $QB_GroupBy = []; + protected ?int $QB_Offset = null; + protected ?int $QB_Limit = null; + + protected array $QB_Join = []; + + protected static int $QBGroupId = 0; + + protected ?string $_QBStatementTemp = null; + + /** @var string */ + protected string $table; + + /** @var string[]|null */ + protected ?array $allowedFields = null; + + public function __destruct() + { + $this->clear(); + } + + /** + * @inheritDoc + */ + public function clear(): self + { + $this->_QBStatementTemp = null; + return $this->reset(); + } + + /** + * @inheritDoc + */ + public function reset(): self + { + static::$QBGroupId = 0; + $this->QB_Select = []; + $this->QB_From = []; + $this->QB_Where = []; + $this->QB_Having = []; + $this->QB_OrderBy = []; + $this->QB_GroupBy = []; + $this->QB_Offset = null; + $this->QB_Limit = null; + $this->QB_Join = []; + return $this; + } + + + /** + * @inheritDoc + */ + public function union(): self + { + $this->_QBStatementTemp = $this->selectStatementBuild() + . ' UNION '; + return $this; + } + + /** + * @inheritDoc + */ + public function unionAll(): self + { + $this->_QBStatementTemp = $this->selectStatementBuild() + . ' UNION ALL '; + return $this; + } + + /** + * @inheritDoc + */ + public function select(string ...$columns): self + { + foreach ($columns as $column) { + $this->prepareSelect($column); + } + return $this; + } + + /** + * @inheritDoc + */ + public function selectCount(string $column): self + { + $this->prepareSelect($column, null, 'count'); + return $this; + } + + /** + * @inheritDoc + */ + public function selectAs(string $column, string $alias): self + { + $this->prepareSelect($column, $alias); + return $this; + } + + /** + * @inheritDoc + */ + public function from(string $table): self + { + $table = trim($table); + if(!isset($this->table)){ + $this->table = $table; + } + $table = $this->_QB_Prefix . $table; + if(in_array($table, $this->QB_From, true) === FALSE){ + $this->QB_From[] = $table; + } + return $this; + } + + /** + * @inheritDoc + */ + public function join(string $table, string $onStmt, string $type = 'INNER'): self + { + $type = strtoupper(trim($type)); + if(in_array($type, $this->_supported_join_types, true) === FALSE){ + throw new \InvalidArgumentException($type . ' Join type is not supported.'); + } + + $table = trim(($this->_QB_Prefix . $table)); + if(isset($this->QB_Join[$table]) || in_array($table, $this->QB_From, true) !== FALSE){ + return $this; + } + $onStmt = str_replace(' = ', '=', $onStmt); + if((bool)preg_match('/([\w\_\-]+)\.([\w\_\-]+)=([\w\_\-]+)\.([\w\_\-]+)/u', $onStmt, $stmt) === FALSE){ + throw new \InvalidArgumentException('Join syntax is not in the correct format. Example : "post.author=user.id"'); + } + + if($type === 'SELF'){ + $this->QB_From[] = $table; + $this->QB_Where[0]['AND'][] = trim(($this->_QB_Prefix . $stmt[1])) . '.' . $stmt[2] + . '=' + . trim(($this->_QB_Prefix . $stmt[3])) . '.' . $stmt[4]; + }else{ + $this->QB_Join[$table] = $type . ' JOIN ' . $table + . ' ON ' + . trim(($this->_QB_Prefix . $stmt[1])) . '.' . $stmt[2] + . '=' + . trim(($this->_QB_Prefix . $stmt[3])) . '.' . $stmt[4]; + } + return $this; + } + + /** + * @inheritDoc + */ + public function selfJoin(string $table, string $onStmt): self + { + return $this->join($table, $onStmt, 'SELF'); + } + + /** + * @inheritDoc + */ + public function innerJoin(string $table, string $onStmt): self + { + return $this->join($table, $onStmt, 'INNER'); + } + + /** + * @inheritDoc + */ + public function leftJoin(string $table, string $onStmt): self + { + return $this->join($table, $onStmt, 'LEFT'); + } + + /** + * @inheritDoc + */ + public function rightJoin(string $table, string $onStmt): self + { + return $this->join($table, $onStmt, 'RIGHT'); + } + + /** + * @inheritDoc + */ + public function leftOuterJoin(string $table, string $onStmt): self + { + return $this->join($table, $onStmt, 'LEFT OUTER'); + } + + /** + * @inheritDoc + */ + public function rightOuterJoin(string $table, string $onStmt): self + { + return $this->join($table, $onStmt, 'RIGHT OUTER'); + } + + /** + * @inheritDoc + */ + public function group(\Closure $group): self + { + ++static::$QBGroupId; + call_user_func_array($group, [$this]); + --static::$QBGroupId; + return $this; + } + + /** + * @inheritDoc + */ + public function where(string $column, $value, string $mark = '=', string $logical = 'AND'): self + { + $logical = str_replace(['&&', '||'], ['AND', 'OR'], strtoupper($logical)); + if(in_array($logical, ['AND', 'OR'], true) === FALSE){ + throw new \InvalidArgumentException('Logical operator OR, AND, && or || it could be.'); + } + $this->QB_Where[static::$QBGroupId][$logical][] = $this->whereOrHavingStatementPrepare($column, $value, $mark); + return $this; + } + + /** + * @inheritDoc + */ + public function andWhereInject(string $statement): self + { + $this->QB_Where[static::$QBGroupId]['AND'][] = $statement; + return $this; + } + + /** + * @inheritDoc + */ + public function orWhereInject(string $statement): self + { + $this->QB_Where[static::$QBGroupId]['OR'][] = $statement; + return $this; + } + + /** + * @inheritDoc + */ + public function andWhere(string $column, $value, string $mark = '='): self + { + return $this->where($column, $value, $mark, 'AND'); + } + + /** + * @inheritDoc + */ + public function orWhere(string $column, $value, string $mark = '='): self + { + return $this->where($column, $value, $mark, 'OR'); + } + + /** + * @inheritDoc + */ + public function having(string $column, $value, string $mark = '', string $logical = 'AND'): self + { + $logical = str_replace(['&&', '||'], ['AND', 'OR'], strtoupper($logical)); + if(in_array($logical, ['AND', 'OR'], true) === FALSE){ + throw new \InvalidArgumentException('Logical operator OR, AND, && or || it could be.'); + } + $this->QB_Having[static::$QBGroupId][$logical][] = $this->whereOrHavingStatementPrepare($column, $value, $mark); + return $this; + } + + /** + * @inheritDoc + */ + public function andHavingInject(string $statement): self + { + $this->QB_Having[static::$QBGroupId]['AND'][] = $statement; + return $this; + } + + /** + * @inheritDoc + */ + public function orHavingInject(string $statement): self + { + $this->QB_Having[static::$QBGroupId]['OR'][] = $statement; + return $this; + } + + /** + * @inheritDoc + */ + public function orderBy(string $column, string $soft = 'ASC'): self + { + $soft = trim(strtoupper($soft)); + if(in_array($soft, ['ASC', 'DESC'], true) === FALSE){ + throw new \InvalidArgumentException('It can only sort as ASC or DESC.'); + } + $orderBy = $this->tableDotColumnSplitAndCombine($column) . ' ' . $soft; + if(in_array($orderBy, $this->QB_OrderBy, true) === FALSE){ + $this->QB_OrderBy[] = $orderBy; + } + return $this; + } + + /** + * @inheritDoc + */ + public function groupBy(string $column): self + { + $group = $this->tableDotColumnSplitAndCombine($column); + if(in_array($group, $this->QB_GroupBy, true) === FALSE){ + $this->QB_GroupBy[] = $group; + } + return $this; + } + + /** + * @inheritDoc + */ + public function offset(int $offset = 0): self + { + if($offset < 0){ + $offset = (int)abs($offset); + } + if($this->QB_Limit === null){ + $this->QB_Limit = 1000; + } + $this->QB_Offset = $offset; + return $this; + } + + /** + * @inheritDoc + */ + public function limit(int $limit): self + { + if($limit < 0){ + $limit = (int)abs($limit); + } + $this->QB_Limit = $limit; + return $this; + } + + /** + * @inheritDoc + */ + public function between(string $column, array $values, string $logical = 'AND'): self + { + return $this->where($column, $values, 'BETWEEN', $logical); + } + + /** + * @inheritDoc + */ + public function orBetween(string $column, array $values): self + { + return $this->where($column, $values, 'BETWEEN', 'OR'); + } + + /** + * @inheritDoc + */ + public function andBetween(string $column, array $values): self + { + return $this->where($column, $values, 'BETWEEN', 'AND'); + } + + /** + * @inheritDoc + */ + public function notBetween(string $column, array $values, string $logical = 'AND'): self + { + return $this->where($column, $values, 'NOT BETWEEN', $logical); + } + + /** + * @inheritDoc + */ + public function orNotBetween(string $column, array $values): self + { + return $this->where($column, $values, 'NOT BETWEEN', 'OR'); + } + + /** + * @inheritDoc + */ + public function andNotBetween(string $column, array $values): self + { + return $this->where($column, $values, 'NOT BETWEEN', 'AND'); + } + + /** + * @inheritDoc + */ + public function findInSet(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'FIND_IN_SET', $logical); + } + + /** + * @inheritDoc + */ + public function orFindInSet(string $column, $value): self + { + return $this->where($column, $value, 'FIND_IN_SET', 'OR'); + } + + /** + * @inheritDoc + */ + public function andFindInSet(string $column, $value): self + { + return $this->where($column, $value, 'FIND_IN_SET', 'AND'); + } + + /** + * @inheritDoc + */ + public function in(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'IN', $logical); + } + + /** + * @inheritDoc + */ + public function orIn(string $column, $value): self + { + return $this->where($column, $value, 'IN', 'OR'); + } + + /** + * @inheritDoc + */ + public function andIn(string $column, $value): self + { + return $this->where($column, $value, 'IN', 'AND'); + } + + /** + * @inheritDoc + */ + public function notIn(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'NOT IN', $logical); + } + + /** + * @inheritDoc + */ + public function orNotIn(string $column, $value): self + { + return $this->where($column, $value, 'NOT IN', 'OR'); + } + + /** + * @inheritDoc + */ + public function andNotIn(string $column, $value): self + { + return $this->where($column, $value, 'NOT IN', 'AND'); + } + + /** + * @inheritDoc + */ + public function like(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'LIKE', $logical); + } + + /** + * @inheritDoc + */ + public function orLike(string $column, $value): self + { + return $this->where($column, $value, 'LIKE', 'OR'); + } + + /** + * @inheritDoc + */ + public function andLike(string $column, $value): self + { + return $this->where($column, $value, 'LIKE', 'AND'); + } + + /** + * @inheritDoc + */ + public function startLike(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'START_LIKE', $logical); + } + + /** + * @inheritDoc + */ + public function orStartLike(string $column, $value): self + { + return $this->where($column, $value, 'START_LIKE', 'OR'); + } + + /** + * @inheritDoc + */ + public function andStartLike(string $column, $value): self + { + return $this->where($column, $value, 'START_LIKE', 'AND'); + } + + /** + * @inheritDoc + */ + public function endLike(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'END_LIKE', $logical); + } + + /** + * @inheritDoc + */ + public function orEndLike(string $column, $value): self + { + return $this->where($column, $value, 'END_LIKE', 'OR'); + } + + /** + * @inheritDoc + */ + public function andEndLike(string $column, $value): self + { + return $this->where($column, $value, 'END_LIKE', 'AND'); + } + + /** + * @inheritDoc + */ + public function notLike(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'NOT LIKE', $logical); + } + + /** + * @inheritDoc + */ + public function orNotLike(string $column, $value): self + { + return $this->where($column, $value, 'NOT LIKE', 'OR'); + } + + /** + * @inheritDoc + */ + public function andNotLike(string $column, $value): self + { + return $this->where($column, $value, 'NOT LIKE', 'AND'); + } + + /** + * @inheritDoc + */ + public function startNotLike(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'START_NOT_LIKE', $logical); + } + + /** + * @inheritDoc + */ + public function orStartNotLike(string $column, $value): self + { + return $this->where($column, $value, 'START_NOT_LIKE', 'OR'); + } + + /** + * @inheritDoc + */ + public function andStartNotLike(string $column, $value): self + { + return $this->where($column, $value, 'START_NOT_LIKE', 'AND'); + } + + /** + * @inheritDoc + */ + public function endNotLike(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'END_NOT_LIKE', $logical); + } + + /** + * @inheritDoc + */ + public function orEndNotLike(string $column, $value): self + { + return $this->where($column, $value, 'END_NOT_LIKE', 'OR'); + } + + /** + * @inheritDoc + */ + public function andEndNotLike(string $column, $value): self + { + return $this->where($column, $value, 'END_NOT_LIKE', 'AND'); + } + + /** + * @inheritDoc + */ + public function soundex(string $column, $value, string $logical = 'AND'): self + { + return $this->where($column, $value, 'SOUNDEX', $logical); + } + + /** + * @inheritDoc + */ + public function orSoundex(string $column, $value): self + { + return $this->where($column, $value, 'SOUNDEX', 'OR'); + } + + /** + * @inheritDoc + */ + public function andSoundex(string $column, $value): self + { + return $this->where($column, $value, 'SOUNDEX', 'AND'); + } + + /** + * @inheritDoc + */ + public function is(string $column, $value = null, string $logical = 'AND'): self + { + return $this->where($column, $value, 'IS', $logical); + } + + /** + * @inheritDoc + */ + public function orIs(string $column, $value = null): self + { + return $this->where($column, $value, 'IS', 'OR'); + } + + /** + * @inheritDoc + */ + public function andIs(string $column, $value = null): self + { + return $this->where($column, $value, 'IS', 'AND'); + } + + /** + * @inheritDoc + */ + public function isNot(string $column, $value = null, string $logical = 'AND'): self + { + return $this->where($column, $value, 'IS NOT', $logical); + } + + /** + * @inheritDoc + */ + public function orIsNot(string $column, $value = null): self + { + return $this->where($column, $value, 'IS NOT', 'OR'); + } + + /** + * @inheritDoc + */ + public function andIsNot(string $column, $value = null): self + { + return $this->where($column, $value, 'IS NOT', 'AND'); + } + + /** + * Constructs and returns an SQL SELECT statement. + * + * @return string + */ + public function selectStatementBuild(): string + { + $sql = ''; + if(!empty($this->_QBStatementTemp)){ + $sql .= trim($this->_QBStatementTemp) . ' '; + } + if(empty($this->QB_Select)){ + $this->QB_Select[] = '*'; + } + + $sql .= 'SELECT ' . implode(', ', $this->QB_Select); + + if(empty($this->QB_From) && isset($this->table)){ + $this->QB_From[] = $this->_QB_Prefix . $this->table; + } + $sql .= ' FROM ' . implode(', ', $this->QB_From); + + if(!empty($this->QB_Join)){ + $sql .= ' ' . implode(' ', $this->QB_Join); + } + + if(!empty($this->QB_Where)){ + $sql .= ' WHERE ' . $this->sqlWhereOrHavingStatementBuild('where'); + } + + if(!empty($this->QB_GroupBy)){ + $sql .= ' GROUP BY ' . implode(', ', $this->QB_GroupBy); + } + + if(!empty($this->QB_Having)){ + $sql .= ' HAVING ' . $this->sqlWhereOrHavingStatementBuild('having'); + } + + if(!empty($this->QB_OrderBy)){ + $sql .= ' ORDER BY ' . implode(', ', $this->QB_OrderBy); + } + + return trim($sql . $this->sqlLimitStatementBuild()); + } + + /** + * Builds and returns an INSERT SQL statement. + * + * @param array $associativeData + * @return string + */ + public function insertStatementBuild(array $associativeData): string + { + $sql = 'INSERT INTO' + . ' ' . (!empty($this->QB_From) ? end($this->QB_From) : ($this->_QB_Prefix . ($this->table ?? ''))); + $columns = []; + $values = []; + if(count($associativeData) === count($associativeData, COUNT_RECURSIVE)){ + foreach ($associativeData as $column => $value) { + $column = trim($column); + if($this->allowedFields !== null && in_array($column, $this->allowedFields, true) === FALSE){ + continue; + } + $columns[] = $column; + $values[] = $this->argumentPrepare($value); + } + $sql .= ' (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ');'; + }else{ + foreach ($associativeData as &$row) { + $value = []; + foreach ($row as $column => $val){ + $column = trim($column); + if($this->allowedFields !== null && in_array($column, $this->allowedFields, true) === FALSE){ + continue; + } + if(in_array($column, $columns, true) === FALSE){ + $columns[] = $column; + } + $value[$column] = $this->argumentPrepare($val); + } + $values[] = $value; + } + $insertValues = []; + + foreach ($values as $value) { + $tmpValue = $value; + $value = []; + foreach ($columns as $column) { + $value[$column] = $tmpValue[$column] ?? 'NULL'; + } + $insertValues[] = '(' . implode(', ', $value) . ')'; + } + $sql .= ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $insertValues) . ';'; + } + return $sql; + } + + /** + * Builds and returns an UPDATE SQL statement. + * + * @param array $associativeData + * @return string + */ + public function updateStatementBuild(array $associativeData): string + { + $sql = 'UPDATE' + . ' ' . (!empty($this->QB_From) ? end($this->QB_From) : ($this->_QB_Prefix . ($this->table ?? ''))); + + $sets = []; + foreach ($associativeData as $column => $value) { + $column = trim($column); + if($this->allowedFields !== null && in_array($column, $this->allowedFields, true) === FALSE){ + continue; + } + $sets[] = $column . ' = '. $this->argumentPrepare($value); + } + if(empty($sets)){ + return ''; + } + $sql .= ' SET ' . implode(', ', $sets) . ' WHERE '; + + $where = $this->sqlWhereOrHavingStatementBuild('where'); + $sql .= (empty($where) ? '1' : $where); + $sql .= $this->sqlLimitStatementBuild(); + return $sql; + } + + /** + * Builds and returns a DELETE SQL statement. + * + * @return string + */ + public function deleteStatementBuild(): string + { + $sql = 'DELETE FROM' + . ' ' . (!empty($this->QB_From) ? end($this->QB_From) : ($this->_QB_Prefix . ($this->table ?? ''))); + + $where = $this->sqlWhereOrHavingStatementBuild('where'); + $sql .= ' WHERE '; + $sql .= (empty($where) ? '1' : $where); + $sql .= $this->sqlLimitStatementBuild(); + return $sql; + } + + private function sqlWhereOrHavingStatementBuild(string $type): string + { + $property = 'QB_' . ucfirst($type); + if(property_exists($this, $property) === FALSE){ + return ''; + } + $statements = $this->{$property}; + if(!is_array($statements) || empty($statements)){ + return ''; + } + return $this->groupWhereOrHavingStatementGenerator($statements, 0); + } + + private function groupWhereOrHavingStatementGenerator($statements, int $level = 0): string + { + $stmt = ''; + if($level != 0){ + $stmt .= ' AND ('; + } + $items = $statements[$level]; + $put = null; + if(isset($items['AND']) && is_array($items['AND']) && !empty($items['AND'])){ + $stmt .= implode(' AND ', $items['AND']); + $put = ' OR '; + } + if(isset($items['OR']) && is_array($items['OR']) && !empty($items['OR'])){ + $stmt .= $put . implode(' OR ', $items['OR']); + } + $nextLevel = $level + 1; + if(isset($statements[$nextLevel])){ + $stmt .= $this->groupWhereOrHavingStatementGenerator($statements, $nextLevel); + } + if($level != 0){ + $stmt .= ')'; + } + return $stmt; + } + + + private function sqlLimitStatementBuild(): string + { + if($this->QB_Limit !== null){ + return ' LIMIT ' + . ($this->QB_Offset !== null ? $this->QB_Offset . ', ' : '') + . $this->QB_Limit; + } + return ''; + } + + private function whereOrHavingStatementPrepare(string $column, $value, string $mark = '='): string + { + $column = $this->tableDotColumnSplitAndCombine($column); + $value = $this->argumentPrepare($value); + $mark = trim($mark); + if($mark === '='){ + return $column . ' = ' . $value; + } + switch (strtoupper($mark)) { + case '!=': + return $column . ' != ' . $value; + case '<': + return $column . ' < ' . $value; + case '<=': + return $column . ' <= ' . $value; + case '>': + return $column . ' > ' . $value; + case '>=': + return $column . ' >= ' . $value; + case 'LIKE' : + return $column . ' LIKE "%' . trim($value, '\\"') . '%"'; + case 'START_LIKE': + return $column . ' LIKE "%' . trim($value, '\\"') . '"'; + case 'END_LIKE': + return $column . ' LIKE "' . trim($value, '\\"') . '%"'; + case 'NOT_LIKE': + case 'NOTLIKE': + case 'NOT LIKE': + return $column . ' NOT LIKE "%' . trim($value, '\\"') . '%"'; + case 'START_NOT_LIKE': + return $column . ' NOT LIKE "%' . trim($value, '\\"') . '"'; + case 'END_NOT_LIKE': + return $column . ' NOT LIKE "' . trim($value, '\\"') . '%"'; + case 'BETWEEN': + return $column . ' BETWEEN ' . $this->betweenArgumentPrepare($value); + case 'NOT_BETWEEN': + case 'NOTBETWEEN': + case 'NOT BETWEEN': + return $column . ' NOT BETWEEN ' . $this->betweenArgumentPrepare($value); + case 'IN': + return $column . ' IN (' . (is_array($value) ? implode(', ', $value) : $value) . ')'; + case 'NOT_IN': + case 'NOTIN': + case 'NOT IN': + return $column . ' NOT IN (' . (is_array($value) ? implode(', ', $value) : $value) . ')'; + case 'FIND IN SET': + case 'FINDINSET': + case 'FIND_IN_SET': + if(is_array($value)){ + foreach ($value as &$val) { + if(is_int($val)){ + continue; + } + $val = trim($val, "\"\\ \t\n\r\0\x0B"); + } + $value = '"'.implode(',', $value).'"'; + } + return 'FIND_IN_SET(' . $column . ', ' . ($value ?? 'NULL') . ')'; + case 'SOUNDEX': + return "SOUNDEX(" . $column . ") LIKE CONCAT('%', TRIM(TRAILING '0' FROM SOUNDEX(" . $value . ")), '%')"; + case 'IS': + return $column . ' IS ' . $value; + case 'IS_NOT': + case 'ISNOT': + case 'IS NOT': + return $column . ' IS NOT ' . $value; + default: + return $column . ' ' . $mark . ' ' .$value; + } + } + + private function argumentPrepare($value) + { + if(is_numeric($value)){ + return $value; + } + if($value === null){ + return 'NULL'; + } + if(is_bool($value)){ + return (int)$value; + } + if($value === '?'){ + return $value; + } + if(is_iterable($value)){ + foreach ($value as &$val) { + $this->escapeString($val); + } + return $value; + } + $value = trim((string)$value); + if(((bool)preg_match('/^:[\w]+$/', $value)) !== FALSE){ + return $value; + } + return '"' . $this->escapeString($value, Connection::ESCAPE_STR) . '"'; + } + + private function betweenArgumentPrepare(array $value): string + { + return (is_numeric($value[0]) ? $value[0] : '"' . $value[0] . '"') + . ' AND ' + . (is_numeric($value[1]) ? $value[1] : '"' . $value[1] . '"'); + } + + private function prepareSelect($column, ?string $alias = null, ?string $fn = null) + { + if(is_array($column)){ + foreach ($column as $item) { + $this->prepareSelect($item); + } + return; + } + $column = trim((string)$column); + if($column === ''){ + return; + } + if(strpos($column, ',') !== FALSE){ + $this->prepareSelect(explode(',', $column)); + return; + } + if(stripos($column, ' as ') !== FALSE){ + $column = str_replace(' AS ', ' as ', $column); + $split = explode(' as ', $column); + $this->prepareSelect($split[0], $split[1]); + return; + } + if(((bool)preg_match('/([\w\_]+)\((.+)\)$/iu', $column, $parse)) !== FALSE){ + $this->prepareSelect($parse[2], $alias, $parse[1]); + return; + } + if($alias !== null){ + $alias = trim($alias); + } + $select = $this->tableDotColumnSplitAndCombine($column); + if(!empty($fn)){ + $select = strtoupper($fn) . '(' .$select . ')'; + } + if(!empty($alias)){ + $select .= ' AS ' . $alias; + } + if(in_array($select, $this->QB_Select, true) === FALSE){ + $this->QB_Select[] = $select; + } + } + + private function tableDotColumnSplitAndCombine(string $tableDotColumn): string + { + if(strpos($tableDotColumn, '.') !== FALSE){ + $split = explode('.', $tableDotColumn, 2); + $table = trim(($this->_QB_Prefix . $split[0])); + return $table . '.' . $split[1]; + } + return $tableDotColumn; + } + +} diff --git a/tests/QueryBuilderUnitTest.php b/tests/QueryBuilderUnitTest.php new file mode 100644 index 0000000..062d10b --- /dev/null +++ b/tests/QueryBuilderUnitTest.php @@ -0,0 +1,307 @@ + + * @copyright Copyright © 2022 Database + * @license http://www.gnu.org/licenses/gpl-3.0.txt GNU GPL 3.0 + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace Test\InitPHP\Database; + +use InitPHP\Database\DB; +use InitPHP\Database\Interfaces\QueryBuilderInterface; + +class QueryBuilderUnitTest extends \PHPUnit\Framework\TestCase +{ + protected QueryBuilderInterface $db; + + protected function setUp(): void + { + $this->db = new DB(['prefix' => 'p_']); + parent::setUp(); + } + + + public function testSelectBuilder() + { + $this->db->select('id', 'name'); + $this->db->from('user'); + + $expected = "SELECT id, name FROM p_user"; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testBlankBuild() + { + $this->db->from('post'); + + $expected = 'SELECT * FROM p_post'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testSelfJoinBuild() + { + $this->db->select('post.id', 'post.title', 'user.name as authorName'); + $this->db->from('post'); + $this->db->selfJoin('user', 'user.id=post.user'); + + $expected = "SELECT p_post.id, p_post.title, p_user.name AS authorName FROM p_post, p_user WHERE p_user.id=p_post.user"; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testInnerJoinBuild() + { + $this->db->select('post.id', 'post.title', 'user.name as authorName'); + $this->db->from('post'); + $this->db->innerJoin('user', 'user.id=post.user'); + + $expected = "SELECT p_post.id, p_post.title, p_user.name AS authorName FROM p_post INNER JOIN p_user ON p_user.id=p_post.user"; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + 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 p_post.id, p_post.title, p_user.name AS authorName FROM p_post LEFT JOIN p_user ON p_user.id=p_post.user"; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + 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 p_post.id, p_post.title, p_user.name AS authorName FROM p_post RIGHT JOIN p_user ON p_user.id=p_post.user"; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + 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 p_post.id, p_post.title, p_user.name AS authorName FROM p_post LEFT OUTER JOIN p_user ON p_user.id=p_post.user"; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + 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 p_post.id, p_post.title, p_user.name AS authorName FROM p_post RIGHT OUTER JOIN p_user ON p_user.id=p_post.user"; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testWhereGrouping() + { + $this->db->from('post') + ->selfJoin('user', 'user.id=post.user') + ->group(function (QueryBuilderInterface $db) { + $db->orWhere('user.group', 'admin'); + $db->orWhere('user.group', 'editor'); + $db->group(function (QueryBuilderInterface $db) { + $db->andWhere('post.publish', true); + $db->andWhere('user.status', true); + }); + }) + ->andWhere('post.status', true); + + $expected = 'SELECT * FROM p_post, p_user WHERE p_user.id=p_post.user AND p_post.status = 1 AND (p_user.group = "admin" OR p_user.group = "editor" AND (p_post.publish = 1 AND p_user.status = 1))'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testHavingStatementBuild() + { + $this->db->select('typeId') + ->selectCount('*') + ->from('book') + ->groupBy('typeId') + ->having('typeId', [1,2,3], 'IN'); + + $expected = 'SELECT typeId, COUNT(*) FROM p_book GROUP BY typeId HAVING typeId IN (1, 2, 3)'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testWhereInjectStatement() + { + $this->db->select('id') + ->from('book') + ->andWhereInject('id = 10') + ->andWhereInject('type != 1 && status = 1') + ->orWhereInject('author = 5'); + + $expected = 'SELECT id FROM p_book WHERE id = 10 AND type != 1 && status = 1 OR author = 5'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testLimitStatement() + { + $this->db->select('id') + ->from('book') + ->limit(5); + + $expected = 'SELECT id FROM p_book LIMIT 5'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testOffsetStatement() + { + $this->db->select('id') + ->from('book') + ->offset(5); + + // Offset is specified If no limit is specified; The limit is 1000. + $expected = 'SELECT id FROM p_book LIMIT 5, 1000'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testOffsetLimitStatement() + { + $this->db->select('id') + ->from('book') + ->offset(50) + ->limit(25); + + $expected = 'SELECT id FROM p_book LIMIT 50, 25'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + 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 p_book LIMIT 25, 20'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testOrderByStatement() + { + $this->db->select('name') + ->from('book') + ->orderBy('authorId', 'ASC') + ->orderBy('id', 'DESC') + ->limit(10); + + $expected = 'SELECT name FROM p_book ORDER BY authorId ASC, id DESC LIMIT 10'; + + $this->assertEquals($expected, $this->db->selectStatementBuild()); + $this->db->clear(); + } + + public function testInsertStatementBuild() + { + $this->db->from('post'); + + $data = [ + 'title' => 'Post Title', + 'content' => 'Post Content', + 'author' => 5, + 'status' => true, + ]; + + $expected = 'INSERT INTO p_post (title, content, author, status) VALUES ("Post Title", "Post Content", 5, 1);'; + $this->assertEquals($expected, $this->db->insertStatementBuild($data)); + $this->db->clear(); + } + + public function testMultiInsertStatementBuild() + { + $this->db->from('post'); + + $data = [ + [ + 'title' => 'Post Title #1', + 'content' => 'Post Content #1', + 'author' => 5, + 'status' => true, + ], + [ + 'title' => 'Post Title #2', + 'content' => 'Post Content #2', + 'status' => false, + ] + ]; + + $expected = 'INSERT INTO p_post (title, content, author, status) VALUES ("Post Title #1", "Post Content #1", 5, 1), ("Post Title #2", "Post Content #2", NULL, 0);'; + $this->assertEquals($expected, $this->db->insertStatementBuild($data)); + $this->db->clear(); + } + + public function testUpdateStatementBuild() + { + $this->db->from('post') + ->where('status', true) + ->limit(5); + + $data = [ + 'title' => 'New Title', + 'status' => false, + ]; + + $expected = 'UPDATE p_post SET title = "New Title", status = 0 WHERE status = 1 LIMIT 5'; + + $this->assertEquals($expected, $this->db->updateStatementBuild($data)); + $this->db->clear(); + } + + public function testDeleteStatementBuild() + { + $this->db->from('post') + ->where('authorId', 5) + ->limit(100); + + $expected = 'DELETE FROM p_post WHERE authorId = 5 LIMIT 100'; + + $this->assertEquals($expected, $this->db->deleteStatementBuild()); + $this->db->clear(); + } + +}