diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8db823e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.idea/ +/.vs/ +/.vscode/ +/vendor/ +/composer.lock +/.phpunit.result.cache \ No newline at end of file diff --git a/README.md b/README.md index 598208e..531ff7b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,405 @@ -# Database -InitPHP Database Library +# InitPHP Database + +Manage your database with or without abstraction. This library is built on the PHP PDO plugin and is mainly used to build and execute SQL queries. + +[![Latest Stable Version](http://poser.pugx.org/initphp/database/v)](https://packagist.org/packages/initphp/database) [![Total Downloads](http://poser.pugx.org/initphp/database/downloads)](https://packagist.org/packages/initphp/database) [![Latest Unstable Version](http://poser.pugx.org/initphp/database/v/unstable)](https://packagist.org/packages/initphp/database) [![License](http://poser.pugx.org/initphp/database/license)](https://packagist.org/packages/initphp/database) [![PHP Version Require](http://poser.pugx.org/initphp/database/require/php)](https://packagist.org/packages/initphp/database) + +PHP has powerful database abstraction libraries like Doctrine and Laravel Eloquent which are pretty nice. However, these are too extensive and cumbersome for projects that only need to easily create and execute database queries. If you don't always need migrations and seeding, this library will do the trick. + +## Requirements + +- PHP 7.4 and above. +- PHP PDO extension. +- [InitPHP Validation](https://github.com/InitPHP/Validation) (Only if you want to use validation operations in Model class.) + +## Supported Databases + +This library should work correctly in almost any database that uses basic SQL syntax. +Databases supported by PDO and suitable drivers are available at [https://www.php.net/manual/en/pdo.drivers.php](https://www.php.net/manual/en/pdo.drivers.php). + +## Installation + +``` +composer require initphp/database --no-dev +``` + +## Usage + +### QueryBuilder and CRUD + +```php +require_once "vendor/autoload.php"; +use \InitPHP\Database\DB; + +// Connection +$db = (new DB([ + 'DSN' => 'mysql:host=localhost;port=3306;dbname=test;charset=utf8mb4', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_general_ci', + 'prefix' => 'blog_', // Table Prefix +]))->connection(); + +// If you are working with a single database, do not forget to make your connection global. +$db->asConnectionGlobal(); +``` + +#### Create + +Single Row : + +```php +$data = [ + 'title' => 'Post Title', + 'content' => 'Post Content', +]; + +/** @var $db \InitPHP\Database\DB */ +$isInsert = $db->from('post') + ->insert($data); + +/** +* This executes the following query. +* +* INSERT INTO blog_post +* (title, content) +* VALUES +* ("Post Title", "Post Content"); +*/ +if($isInsert){ + // Success +} +``` + +Multi Row: + +```php +$data = [ + [ + 'title' => 'Post Title 1', + 'content' => 'Post Content 1', + 'author' => 5 + ], + [ + 'title' => 'Post Title 2', + 'content' => 'Post Content 2' + ], +]; + +/** @var $db \InitPHP\Database\DB */ +$isInsert = $db->from('post') + ->insert($data); + +/** +* This executes the following query. +* +* INSERT INTO blog_post +* (title, content, author) +* VALUES +* ("Post Title 1", "Post Content 1", 5), +* ("Post Title 2", "Post Content 2", NULL); +*/ + +if($isInsert){ + // Success +} +``` + +#### Read + +```php +/** @var $db \InitPHP\Database\DB */ +$db->select('user.name as author_name', 'post.id', 'post.title') + ->from('post') + ->selfJoin('user', 'user.id=post.author') + ->where('post.status', true) + ->orderBy('post.id', 'ASC') + ->orderBy('post.created_at', 'DESC') + ->offset(20)->limit(10); + +/** +* This executes the following query. +* +* SELECT blog_user.name AS author_name, blog_post.id, blog_post.title +* FROM blog_post, blog_user +* WHERE blog_user.id = blog_post.author AND blog_post.status = 1 +* ORDER BY blog_post ASC, blog_post.created_at DESC +* LIMIT 20, 10 +*/ +$db->get(); // Install the SQL statement, execute, and reset the Query Builder. +if ($db->numRows() > 0) { + foreach ($db->rows() as $row) { + echo $row->title . ' by ' . $row->author_name . '
'; + } +} +``` + +#### Update + +```php +$data = [ + 'title' => 'New Title', + 'content' => 'New Content', +]; + +/** @var $db \InitPHP\Database\DB */ +$isUpdate = $db->from('post') + ->where('id', 13) + ->update($data); + +/** +* This executes the following query. +* +* UPDATE blog_post +* SET title = "New Title", content = "New Content" +* WHERE id = 13 +*/ +if ($isUpdate) { + // Success +} +``` + +#### Delete + +```php +/** @var $db \InitPHP\Database\DB */ +$isDelete = $db->from('post') + ->where('id', 13) + ->delete(); + +/** +* This executes the following query. +* +* DELETE FROM blog_post WHERE id = 13 +*/ +if ($isUpdate) { + // Success +} +``` + +### Model and Entity + +Model and Entity; are two common concepts used in database abstraction. To explain these two concepts in the roughest way; + +- **Model :** Each model is a class that represents a table in the database. +- **Entity :** Entity is a class that represents a single row of data. + +The most basic example of a model class would look like this. + +```php +namespace App\Model; + +use \InitPHP\Database\Model; + +class Posts extends Model +{ + + /** + * Only if you don't have global connectivity. + * + * @var array|string[] + */ + protected array $connection = [ + 'DSN' => '', // Database connection address. + 'username' => '', // Username with required privileges in the database. + 'password' => '', // The password of the database user. + 'charset' => 'utf8mb4', // The character set to use in the database. + 'collation' => 'utf8mb4_general_ci', // Collection set to use in database + 'prefix' => '' // Prefix to be used in table names + ]; + + /** + * If not specified, \InitPHP\Database\Entity::class is used by default. + * + * @var EntityInterface|string + */ + protected $entity = \App\Entities\PostEntity::class; + + /** + * If not specified, the name of your model class is used. + * + * @var string + */ + protected string $table = 'post'; + + /** + * The name of the PRIMARY KEY column. If not, define it as NULL. + * + * @var null|string + */ + protected ?string $primaryKey = 'id'; + + /** + * Specify FALSE if you want the data to be permanently deleted. + * + * @var bool + */ + protected bool $useSoftDeletes = true; + + /** + * Column name to hold the creation time of the data. + * + * @var string|null + */ + protected ?string $createdField = 'created_at'; + + /** + * The column name to hold the last time the data was updated. + * + * @var string|null + */ + protected ?string $updatedField = 'updated_at'; + + /** + * Column name to keep deletion time if $useSoftDeletes is active. + * + * @var string|null + */ + 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; + + protected bool $deletable = true; + + protected bool $updatable = true; + + protected array $validation = [ + 'id' => 'is_unique|integer', // Validation methods can be string separated by a perpendicular line. + 'title' => ['required', 'string'], // Validation methods can be given as an array. + ]; + + 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', + // ... + ]; + +} +``` + +The most basic example of a entity class would look like this. + +```php +namespace App\Entities; + +use \InitPHP\Database\Entity; + +class PostEntity extends Entity +{ + /** + * An example of a getter method for the "post_title" column. + * + * Usage : + * echo $entity->post_title; + */ + public function getPostTitleAttribute($title) + { + return strtoupper($title); + } + + /** + * An example of a setter method for the "post_title" column. + * + * Usage : + * $entity->post_title = 'New Post Title'; + */ + public function setPostTitleAttribute($title) + { + $this->post_title = strtolower($title); + } + +} +``` + +## To Do + +- [ ] A more detailed documentation will be prepared. + +## Getting Help + +If you have questions, concerns, bug reports, etc, please file an issue in this repository's Issue Tracker. + +## Getting Involved + +> All contributions to this project will be published under the MIT License. By submitting a pull request or filing a bug, issue, or feature request, you are agreeing to comply with this waiver of copyright interest. + +There are two primary ways to help: + +- Using the issue tracker, and +- Changing the code-base. + +### Using the issue tracker + +Use the issue tracker to suggest feature requests, report bugs, and ask questions. This is also a great way to connect with the developers of the project as well as others who are interested in this solution. + +Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in the issue that you will take on that effort, then follow the Changing the code-base guidance below. + +### Changing the code-base + +Generally speaking, you should fork this repository, make changes in your own fork, and then submit a pull request. All new code should have associated unit tests that validate implemented features and the presence or lack of defects. Additionally, the code should follow any stylistic and architectural guidelines prescribed by the project. In the absence of such guidelines, mimic the styles and patterns in the existing code-base. + +## Credits + +- [Muhammet ŞAFAK](https://www.muhammetsafak.com.tr) <> + +## License + +Copyright © 2022 [MIT License](./LICENSE) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b103e83 --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "initphp/database", + "description": "PHP PDO Database Library", + "keywords": ["php", "pdo", "orm", "model", "entity", "query-builder", "database", "dbal"], + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "InitPHP\\Database\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Test\\InitPHP\\Database\\": "tests/" + } + }, + "authors": [ + { + "name": "Muhammet ŞAFAK", + "email": "info@muhammetsafak.com.tr", + "role": "Developer", + "homepage": "https://www.muhammetsafak.com.tr" + } + ], + "minimum-stability": "stable", + "require": { + "php": ">=7.4", + "ext-pdo": "*", + "initphp/validation": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "9.5" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..bc51414 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + + tests + + + \ No newline at end of file diff --git a/src/Connection.php b/src/Connection.php new file mode 100644 index 0000000..02fc16d --- /dev/null +++ b/src/Connection.php @@ -0,0 +1,259 @@ + + * @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 \PDO; +use \InitPHP\Database\Exception\ConnectionException; +use \InitPHP\Database\Interfaces\ConnectionInterface; + +use function is_numeric; +use function is_string; +use function preg_split; +use function trim; +use function preg_replace; +use function is_array; +use function is_iterable; + +class Connection implements ConnectionInterface +{ + + public const ESCAPE_MIXED = 0; + public const ESCAPE_NUM = 1; + public const ESCAPE_STR = 2; + public const ESCAPE_NUM_ARRAY = 3; + public const ESCAPE_STR_ARRAY = 4; + + protected string $_DSN; + + protected string $_Username; + + protected string $_Password; + + protected string $_Charset = 'utf8mb4'; + + protected string $_Collation = 'utf8mb4_general_ci'; + + protected ?PDO $_PDO = null; + + protected static ?PDO $_staticPDO = null; + + public function __construct(array $configs = []) + { + foreach ($configs as $key => $value) { + $method = 'set' . ucfirst($key); + if(method_exists($this, $method) === FALSE){ + continue; + } + $this->{$method}($value); + } + } + + public function __call($name, $arguments) + { + return $this->getPDO()->{$name}(...$arguments); + } + + /** + * @inheritDoc + */ + public function connection(): self + { + if($this->_PDO === null){ + try { + $this->_PDO = new PDO($this->getDSN(), $this->getUsername(), $this->getPassword(), [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false + ]); + $this->_PDO->exec("SET NAMES '" . $this->getCharset() . "' COLLATE '" . $this->getCollation() . "'"); + $this->_PDO->exec("SET CHARACTER SET '" . $this->getCharset() . "'"); + } catch (\PDOException $e) { + throw new ConnectionException('Connection failed : ' . $e->getMessage()); + } + } + return $this; + } + + /** + * @inheritDoc + */ + public function disconnection() + { + $this->_PDO = null; + static::$_staticPDO = null; + } + + /** + * @inheritDoc + */ + public function asConnectionGlobal() + { + if($this->_PDO === null){ + $this->connection(); + } + static::$_staticPDO = $this->_PDO; + } + + /** + * @inheritDoc + */ + public function getPDO(): \PDO + { + if(static::$_staticPDO !== null){ + return static::$_staticPDO; + } + if($this->_PDO === null){ + $this->connection(); + } + return $this->_PDO; + } + + /** + * @inheritDoc + */ + public function getDSN(): string + { + return $this->_DSN ?? 'mysql:host=localhost'; + } + + /** + * @inheritDoc + */ + public function setDSN(string $DSN): self + { + $this->_DSN = $DSN; + return $this; + } + + /** + * @inheritDoc + */ + public function getUsername(): string + { + return $this->_Username ?? 'root'; + } + + /** + * @inheritDoc + */ + public function setUsername(string $username): self + { + $this->_Username = $username; + return $this; + } + + /** + * @inheritDoc + */ + public function getPassword(): string + { + return $this->_Password ?? ''; + } + + /** + * @inheritDoc + */ + public function setPassword(string $password): self + { + $this->_Password = $password; + return $this; + } + + /** + * @inheritDoc + */ + public function getCharset(): string + { + return $this->_Charset ?? 'utf8mb4'; + } + + /** + * @inheritDoc + */ + public function setCharset(string $charset = 'utf8mb4'): self + { + $this->_Charset = $charset; + return $this; + } + + /** + * @inheritDoc + */ + public function getCollation(): string + { + return $this->_Collation ?? 'utf8mb4_general_ci'; + } + + /** + * @inheritDoc + */ + public function setCollation(string $collation = 'utf8mb4_general_ci'): self + { + $this->_Collation = $collation; + return $this; + } + + public function escapeString($value, int $type = self::ESCAPE_MIXED) + { + switch ($type) { + case self::ESCAPE_NUM: + if(is_numeric($value)){ + return $value; + } + if(is_string($value)){ + $number = ''; + $split = preg_split('/(.*)/i', trim($value), -1, PREG_SPLIT_NO_EMPTY); + foreach ($split as $char){ + if(!is_numeric($char)){ + break; + } + $number .= $char; + } + return (int)$number; + } + return 0; + case self::ESCAPE_STR: + $value = trim((string)$value, "\\ \"'\t\n\r\0\x0B\x00\x0A\x0D\x1A\x22\x27\x5C"); + return preg_replace('/[\x00\x0A\x0D\x1A\x22\x27\x5C]/u', '\\\$0', $value); + case self::ESCAPE_NUM_ARRAY: + if(is_array($value)){ + foreach ($value as &$val) { + $val = $this->escapeString($val, self::ESCAPE_NUM); + } + return $value; + } + return [0]; + case self::ESCAPE_STR_ARRAY: + if(is_array($value)){ + foreach ($value as &$val) { + $val = $this->escapeString($val, self::ESCAPE_STR); + } + return $value; + } + return ['']; + default: + if(is_numeric($value)){ + return $value; + } + if(is_iterable($value)){ + return $this->escapeString($value, self::ESCAPE_STR_ARRAY); + } + return $this->escapeString($value, self::ESCAPE_STR); + } + } + +} diff --git a/src/DB.php b/src/DB.php new file mode 100644 index 0000000..f72ef91 --- /dev/null +++ b/src/DB.php @@ -0,0 +1,375 @@ + + * @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\QueryExecuteException; +use \InitPHP\Database\Interfaces\{DBInterface, EntityInterface}; + +use function is_string; +use function is_object; +use function trim; + +class DB extends Connection implements DBInterface +{ + + use QueryBuilder; + + protected ?string $_DBExecuteLastQueryStatement; + + protected ?int $_DB_NumRows = null, $_DB_LastInsertId = null; + + /** @var string|EntityInterface */ + protected $entity; + + protected ?int $_DBAsReturnType; + protected \PDOStatement $_DBLastStatement; + private array $_DBArguments = []; + + private bool $_DBTransactionStatus = false; + private bool $_DBTransactionTestMode = false; + private bool $_DBTransaction = false; + + public function __construct(array $configs = []) + { + if(isset(static::$_QB_StaticPrefix)){ + $this->_QB_Prefix = static::$_QB_StaticPrefix; + } + if(isset($configs['prefix'])){ + $this->_QB_Prefix = $configs['prefix']; + unset($configs['prefix']); + } + parent::__construct($configs); + } + + public function asConnectionGlobal() + { + static::$_QB_StaticPrefix = $this->_QB_Prefix; + parent::asConnectionGlobal(); + } + + /** + * @inheritDoc + */ + public function numRows($dbOrPDOStatement = null): int + { + if($dbOrPDOStatement === null){ + return $this->_DB_NumRows ?? 0; + } + if($dbOrPDOStatement instanceof \PDOStatement){ + return $dbOrPDOStatement->rowCount(); + } + if($dbOrPDOStatement instanceof DBInterface){ + return $dbOrPDOStatement->numRows(null); + } + return 0; + } + + /** + * @inheritDoc + */ + public function lastSQL(): ?string + { + return $this->_DBExecuteLastQueryStatement ?? null; + } + + /** + * @inheritDoc + */ + public function insertId(): ?int + { + return $this->_DB_LastInsertId ?? null; + } + + /** + * @inheritDoc + */ + public function asAssoc(): self + { + $this->_DBAsReturnType = \PDO::FETCH_ASSOC; + return $this; + } + + /** + * @inheritDoc + */ + public function asArray(): self + { + $this->_DBAsReturnType = \PDO::FETCH_BOTH; + return $this; + } + + /** + * @inheritDoc + */ + public function asObject(): self + { + $this->_DBAsReturnType = \PDO::FETCH_OBJ; + return $this; + } + + /** + * @inheritDoc + */ + public function transactionStatus(): bool + { + return $this->_DBTransactionStatus; + } + + /** + * @inheritDoc + */ + public function transactionStart(bool $testMode = false): self + { + $this->_DBTransaction = true; + $this->_DBTransactionTestMode = $testMode; + $this->_DBTransactionStatus = true; + $this->getPDO()->beginTransaction(); + return $this; + } + + /** + * @inheritDoc + */ + public function transactionComplete(): self + { + $this->_DBTransaction = false; + if($this->_DBTransactionTestMode === FALSE && $this->_DBTransactionStatus !== FALSE){ + $this->getPDO()->commit(); + }else{ + $this->getPDO()->rollBack(); + } + return $this; + } + + /** + * @inheritDoc + */ + public function rows() + { + return $this->dbGetFetchMode('fetchAll'); + } + + /** + * @inheritDoc + */ + public function row() + { + return $this->dbGetFetchMode('fetch'); + } + + /** + * @inheritDoc + */ + public function column(int $column = 0) + { + if(!isset($this->_DBLastStatement)){ + throw new \RuntimeException('The query must be executed with the DB::get() method before the column is retrieved.'); + } + return $this->_DBLastStatement->fetchColumn($column); + } + + /** + * @inheritDoc + */ + public function setParams(array $arguments): self + { + $this->_DBArguments = $arguments; + return $this; + } + + /** + * @inheritDoc + */ + public function get(?string $table = null) + { + if(!empty($table)){ + $this->from($table); + } + $arguments = !empty($this->_DBArguments) ? $this->_DBArguments : null; + $this->_DBArguments = []; + if(($stmt = $this->query($this->selectStatementBuild(), $arguments)) !== FALSE){ + $this->clear(); + return $this->_DBLastStatement = $stmt; + } + return false; + } + + /** + * @inheritDoc + */ + public function fromGet($dbOrPDOStatement): DBInterface + { + $clone = clone $this; + $clone->clear(); + if($dbOrPDOStatement instanceof \PDOStatement){ + $clone->_DBLastStatement = $dbOrPDOStatement; + }elseif($dbOrPDOStatement instanceof DBInterface){ + $clone->_DBLastStatement = $dbOrPDOStatement->_DBLastStatement; + }else{ + throw new \InvalidArgumentException('Get must be a \\PDOStatement or \\SimpleDB\\DB object.'); + } + return $clone; + } + + /** + * @inheritDoc + */ + public function count(): int + { + return $this->exec($this->selectStatementBuild()); + } + + /** + * @inheritDoc + */ + public function query(string $sql, ?array $parameters = null) + { + if(trim($sql) === ''){ + return false; + } + $this->_DBExecuteLastQueryStatement = $sql; + try { + if(($query = $this->getPDO()->prepare($sql)) === FALSE){ + if($this->_DBTransaction !== FALSE){ + $this->_DBTransactionStatus = false; + } + return false; + } + $res = $query->execute((empty($parameters) ? null : $parameters)); + if($res === FALSE && $this->_DBTransaction !== FALSE){ + $this->_DBTransactionStatus = false; + } + }catch (\PDOException $e) { + $this->_DBTransactionStatus = false; + $err = $e->getMessage() . ' ' . PHP_EOL . 'SQL Statement : ' . $sql; + throw new QueryExecuteException($err); + } + if($query instanceof \PDOStatement){ + $this->_DB_NumRows = $query->rowCount(); + if(($insertId = $this->getPDO()->lastInsertId()) !== FALSE){ + $this->_DB_LastInsertId = (int)$insertId; + } + } + return $query; + } + + /** + * Veritabanına bir ya da daha fazla veri ekler. + * + * @param array $data + * @return bool + */ + public function insert(array $data) + { + $stmt = $this->query($this->insertStatementBuild($data)); + $this->clear(); + return $stmt !== FALSE; + } + + /** + * Güncelleme sorgusu çalıştırır. + * + * @param array $data + * @return bool + */ + public function update(array $data) + { + $stmt = $this->query($this->updateStatementBuild($data)); + $this->clear(); + return $stmt !== FALSE; + } + + /** + * Silme sorgusu çalıştırır. + * + * @return bool + */ + public function delete() + { + $stmt = $this->query($this->deleteStatementBuild()); + $this->clear(); + return $stmt !== FALSE; + } + + /** + * @inheritDoc + */ + public function exec(string $sql): int + { + if(trim($sql) === ''){ + return 0; + } + $this->_DBExecuteLastQueryStatement = $sql; + try { + $arguments = !empty($this->_DBArguments) ? $this->_DBArguments : null; + if($arguments !== null){ + if(($stmt = $this->getPDO()->prepare($sql)) === FALSE){ + if($this->_DBTransaction !== FALSE){ + $this->_DBTransactionStatus = false; + } + return 0; + } + if($stmt->execute($arguments) === FALSE && $this->_DBTransaction !== FALSE){ + $this->_DBTransactionStatus = false; + } + return $stmt->rowCount(); + } + if(($stmt = $this->getPDO()->exec($sql)) === FALSE){ + if($this->_DBTransaction !== FALSE){ + $this->_DBTransactionStatus = false; + } + return 0; + } + return (int)$stmt; + }catch (\PDOException $e) { + $this->_DBTransactionStatus = false; + $err = $e->getMessage() + . ' SQL Statement : ' . $sql; + throw new QueryExecuteException($err); + } + } + + /** + * Bu yöntem DB::get() ile yürütülmüş sorgunun yanıtlarını PDOStatement nesnesinden istenen yöntem ile alır. + * + * @used-by DB::rows() + * @used-by DB::row() + * @param string $pdoMethod

[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 bool

Transaction 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 $table

Belirtilirse; 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 $sql

SQL Statement

+ * @param array|null $parameters

Varsa, PDO::execute() yöntemine gönderilecek parametre dizisi.

+ * @return \PDOStatement|false + * @throws QueryExecuteException

SQL 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 $sql

SQL Statement

+ * @return int + * @throws QueryExecuteException

SQL 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 ModelPermissionException

Model'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 ModelPermissionException

Model'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 $id

Varsa PRIMARY KEY sütunun değeri

+ * @return array|false + * @throws ModelPermissionException

Model'in silme izni yoksa.

+ */ + public function delete($id = null); + + /** + * İlk satırı döndürür. + * + * @return EntityInterface|object|array|false + * @throws ModelPermissionException

Model'in veri okuma izni yoksa.

+ */ + public function first(); + + /** + * Bir veriyi arar ve döndürür. + * + * @param null|int|string $id

Varsa PRIMARY KEY sütununun değeri.

+ * @return EntityInterface|object|array|false + * @throws ModelPermissionException

Model'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 ModelPermissionException

Model'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 ModelPermissionException

Model'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 ModelPermissionException

Model'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 $onStmt

Example : "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 $table

The table name to include.

+ * @param string $onStmt

Example : "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 $table

The name of the table to join.

+ * @param string $onStmt

Example : "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 $table

The name of the table to join.

+ * @param string $onStmt

Example : "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 $table

The name of the table to join.

+ * @param string $onStmt

Example : "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 $table

The name of the table to join.

+ * @param string $onStmt

Example : "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 $table

The name of the table to join.

+ * @param string $onStmt

Example : "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 $group

QueryBuilderInterface 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 \InvalidArgumentException

If $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(); + } + +}