From 47c0be00fb2b492eb00c7b5a2bd3a9cc0ae5fe4a Mon Sep 17 00:00:00 2001 From: sheychen Date: Wed, 11 Jul 2018 14:39:17 +0200 Subject: [PATCH] WIP --- .gitignore | 1 + LICENSE | 42 +- composer.json | 35 +- .../Migrations/20180710.0-CreateUserModel.php | 25 + .../20180710.1-UserModelAddUnique.php | 10 + .../20180710.2-CreateOfficeModel.php | 11 + exemple/Model/OfficeModel.php | 10 + exemple/Model/UserModel.php | 35 + exemple/index.php | 17 + src/Connection.php | 154 +- src/Database.php | 42 +- src/DatabaseException.php | 7 - src/Migration.php | 12 + src/Model.php | 1890 +++++++++-------- src/ModelID.php | 115 + src/Request/Create.php | 187 +- src/Request/Data.php | 40 +- src/Request/Delete.php | 76 +- src/Request/Drop.php | 50 +- src/Request/Insert.php | 64 +- src/Request/Request.php | 57 +- src/Request/Select.php | 195 +- src/Request/Update.php | 88 +- src/TypeHelper.php | 21 + 24 files changed, 1804 insertions(+), 1380 deletions(-) create mode 100644 .gitignore create mode 100644 exemple/Model/Migrations/20180710.0-CreateUserModel.php create mode 100644 exemple/Model/Migrations/20180710.1-UserModelAddUnique.php create mode 100644 exemple/Model/Migrations/20180710.2-CreateOfficeModel.php create mode 100644 exemple/Model/OfficeModel.php create mode 100644 exemple/Model/UserModel.php create mode 100644 exemple/index.php delete mode 100644 src/DatabaseException.php create mode 100644 src/Migration.php create mode 100644 src/ModelID.php create mode 100644 src/TypeHelper.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5657f6e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor \ No newline at end of file diff --git a/LICENSE b/LICENSE index 59d50c5..08b4721 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2017 sheychen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2017 sheychen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/composer.json b/composer.json index 8ca5787..0d3fc53 100644 --- a/composer.json +++ b/composer.json @@ -1,18 +1,17 @@ -{ - "name": "krutush/database", - "license": "MIT", - "authors": [ - { - "name": "sheychen", - "email": "contact@clementbois.fr" - } - ], - "require": { - "krutush/krutush": "dev-develop" - }, - "autoload": { - "psr-4": { - "Krutush\\Database\\": "src/" - } - } -} +{ + "name": "krutush/database", + "license": "MIT", + "authors": [ + { + "name": "sheychen", + "email": "contact@clementbois.fr" + } + ], + "require": {}, + "autoload": { + "psr-4": { + "Krutush\\Database\\": "src/", + "Krutush\\Database\\Exemple\\": "exemple/" + } + } +} \ No newline at end of file diff --git a/exemple/Model/Migrations/20180710.0-CreateUserModel.php b/exemple/Model/Migrations/20180710.0-CreateUserModel.php new file mode 100644 index 0000000..d16fa84 --- /dev/null +++ b/exemple/Model/Migrations/20180710.0-CreateUserModel.php @@ -0,0 +1,25 @@ +$migration->create( + Krutush\Database\Exemple\Model\UserModel:class, + Krutush\Database\ModelID:class, + [ + 'id' => [ + 'column' => 'user_id', + 'type' => 'bigint' + ], + 'first_name' => [ + 'not_null' => true + ], + 'last_name' => [ + 'not_null' => true + ], + 'supervisor' => [ + 'foreign' => [] + ], + 'supervised' => [ + 'virtual' => true, + 'foreign' => [ + 'key' => 'supervisor', + 'multiple' => true + ] + ] + ]); \ No newline at end of file diff --git a/exemple/Model/Migrations/20180710.1-UserModelAddUnique.php b/exemple/Model/Migrations/20180710.1-UserModelAddUnique.php new file mode 100644 index 0000000..bca794a --- /dev/null +++ b/exemple/Model/Migrations/20180710.1-UserModelAddUnique.php @@ -0,0 +1,10 @@ +$migration->alter( + Krutush\Database\Exemple\Model\UserModel:class, + [ + 'first_name' => [ + 'unique' => 'full_name' + ], + 'last_name' => [ + 'unique' => 'full_name' + ] + ]); \ No newline at end of file diff --git a/exemple/Model/Migrations/20180710.2-CreateOfficeModel.php b/exemple/Model/Migrations/20180710.2-CreateOfficeModel.php new file mode 100644 index 0000000..2cc2066 --- /dev/null +++ b/exemple/Model/Migrations/20180710.2-CreateOfficeModel.php @@ -0,0 +1,11 @@ +$migration->create( + Krutush\Database\Exemple\Model\OfficeModel:class, + Krutush\Database\ModelID:class, + ['name'] +)->alter( + Krutush\Database\Exemple\Model\UserModel:class, + [ + 'office' => [ + 'foreign' => Krutush\Database\Exemple\Model\OfficeModel:class, + ] + ]); \ No newline at end of file diff --git a/exemple/Model/OfficeModel.php b/exemple/Model/OfficeModel.php new file mode 100644 index 0000000..004c887 --- /dev/null +++ b/exemple/Model/OfficeModel.php @@ -0,0 +1,10 @@ + [ + 'column' => 'user_id', + 'type' => 'bigint' + ], + 'first_name' => [ + 'not_null' => true, + 'unique' => 'full_name' + ], + 'last_name' => [ + 'not_null' => true, + 'unique' => 'full_name' + ], + 'supervisor' => [ + 'foreign' => [] + ], + 'supervised' => [ + 'virtual' => true, + 'index' => true, + 'foreign' => [ + 'key' => 'supervisor', + 'multiple' => true + ] + ], + 'office' => [ + 'foreign' => OfficeModel::class, + ] + ]; +} \ No newline at end of file diff --git a/exemple/index.php b/exemple/index.php new file mode 100644 index 0000000..87abf75 --- /dev/null +++ b/exemple/index.php @@ -0,0 +1,17 @@ +connect(); + +$UserModel = Krutush\Database\Exemple\Model\UserModel::class; + +var_dump($UserModel::getTable()); +//var_dump($UserModel::getFields()); + +var_dump((new $UserModel([ + 'id' => 42, + 'first_name' => 'Pierre De La VIEDDDDDDD', + 'last_name' => 'Caillou' +]))->insert()); + +//TODO: format src before commit \ No newline at end of file diff --git a/src/Connection.php b/src/Connection.php index 319dbe8..4ee85ac 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -1,74 +1,82 @@ -settings = include($path); - } - - public function connect(string $dbname = null){ - if(static::exists($dbname)) - throw new DatabaseException("Allready connect"); - - $dbname = static::parseName($dbname); - if(!isset($this->settings[$dbname])) - throw new DatabaseException('Can\'t find '.$dbname.' in settings'); - - static::$databases[$dbname] = new Database($this->settings[$dbname]); - return static::$databases[$dbname]; - } - - public function tryConnect(string $dbname = null, bool $quiet = false) { - try { - return $this->connect($dbname); - } catch (DatabaseException $e) { - return $quiet ? false : $e; - } - } - - public static function get(string $dbname = null){ - $dbname = static::parseName($dbname); - if(!static::exists($dbname)) - throw new DatabaseException('Can\'t find "'.$dbname.'"'); - - return static::$databases[$dbname]; - } - - public function getCreate(string $dbname = null){ - if(!static::exists($dbname)){ - $this->create($dbname); - } - return static::$databases[static::parseName($dbname)]; - } - - public static function tryGet(string $dbname = null, bool $quiet = false) { - try { - return static::get($dbname); - } catch (DatabaseException $e) { - return $quiet ? false : $e; - } - } - - public function tryGetCreate(string $dbname = null, bool $quiet = false) { - try { - return $this->getCreate($dbname); - } catch (DatabaseException $e) { - return $quiet ? false : $e; - } - } - - public static function exists(string $dbname = null){ - $dbname = static::parseName($dbname); - return isset(static::$databases[$dbname]); - } - - private static function parseName(string $dbname = null){ - return $dbname ?: 'default'; //Edit me - } +settings = include($path); + } + + /** Setup Database */ + public function connect(string $dbname = null): Database{ + if(static::exists($dbname)) + throw new \InvalidArgumentException("Allready connect"); + + $dbname = static::parseName($dbname); + if(!isset($this->settings[$dbname])) + throw new \InvalidArgumentException('Can\'t find '.$dbname.' in settings'); + + static::$databases[$dbname] = new Database($this->settings[$dbname]); + return static::$databases[$dbname]; + } + + /** If you dont remember how to make a try-catch */ + public function tryConnect(string $dbname = null, bool $quiet = false) { + try { + return $this->connect($dbname); + } catch (\Exception $e) { + return $quiet ? false : $e; + } + } + + public static function get(string $dbname = null){ + $dbname = static::parseName($dbname); + if(!static::exists($dbname)) + throw new \InvalidArgumentException('Can\'t find "'.$dbname.'"'); + + return static::$databases[$dbname]; + } + + public function getCreate(string $dbname = null){ + if(!static::exists($dbname)){ + $this->create($dbname); + } + return static::$databases[static::parseName($dbname)]; + } + + public static function tryGet(string $dbname = null, bool $quiet = false) { + try { + return static::get($dbname); + } catch (\Exception $e) { + return $quiet ? false : $e; + } + } + + public function tryGetCreate(string $dbname = null, bool $quiet = false) { + try { + return $this->getCreate($dbname); + } catch (\Exception $e) { + return $quiet ? false : $e; + } + } + + public static function exists(string $dbname = null){ + $dbname = static::parseName($dbname); + return isset(static::$databases[$dbname]); + } + + private static function parseName(string $dbname = null){ + return $dbname ?: 'default'; //Edit me + } } \ No newline at end of file diff --git a/src/Database.php b/src/Database.php index bf2c6d8..711ceb7 100644 --- a/src/Database.php +++ b/src/Database.php @@ -2,11 +2,23 @@ namespace Krutush\Database; +/** + * Extention around PDO + */ class Database{ + /** @var \PDO */ private $pdo; - private $debug = false; + + /** + * Requests history if debug == true + * + * @var array + */ private $requests = []; + /** @var bool */ + private $debug = false; + public function __construct(array $settings){ $dns = $settings['driver'] . ':host=' . $settings['host'] . @@ -23,22 +35,29 @@ class Database{ } public function prepare(string $request){ - if($this->debug) - $this->requests[] = $request; - return $this->pdo->prepare($request); } public function execute(string $request, array $values = null, $row = false){ + if($this->debug) + $time_start = microtime(true); + $req = $this->prepare($request); $req->execute($values); - if($row == false) + if(!$row) return $req->fetchAll(); + if($this->debug) + $this->requests[] = [ + 'request' => $request, + 'values' => $values, + 'fetch' => !$row, + 'time' => round((microtime(true) - $time_start) * 1000) + ]; return $req; } - public function select(array $fields = null){ + public function select(array $fields = null): Request\Select{ $select = new Request\Select($this); if(isset($fields)) return $select->fields($fields); @@ -46,7 +65,7 @@ class Database{ return $select; } - public function insert(array $fields = null){ + public function insert(array $fields = null): Request\Insert{ $insert = new Request\Insert($this); if(isset($fields)) return $insert->fields($fields); @@ -54,7 +73,7 @@ class Database{ return $insert; } - public function update(array $fields = null){ + public function update(array $fields = null): Request\Update{ $update = new Request\Update($this); if(isset($fields)) return $update->fields($fields); @@ -62,7 +81,7 @@ class Database{ return $update; } - public function create(string $table = null){ + public function create(string $table = null): Request\Create{ $create = new Request\Create($this); if(isset($table)) return $create->table($table); @@ -70,7 +89,7 @@ class Database{ return $create; } - public function drop(string $table = null){ + public function drop(string $table = null): Request\Drop{ $drop = new Request\Drop($this); if(isset($table)) return $drop->table($table); @@ -78,7 +97,7 @@ class Database{ return $drop; } - public function delete(){ + public function delete(): Request\Delete{ return new Request\Delete($this); } @@ -89,5 +108,4 @@ class Database{ public function getRequests(): array{ return $this->requests; } - //TODO update, delete } \ No newline at end of file diff --git a/src/DatabaseException.php b/src/DatabaseException.php deleted file mode 100644 index 54229d0..0000000 --- a/src/DatabaseException.php +++ /dev/null @@ -1,7 +0,0 @@ - +- roolback +- new +- create +- status +*/ \ No newline at end of file diff --git a/src/Model.php b/src/Model.php index 7f30cdf..cfc60dd 100644 --- a/src/Model.php +++ b/src/Model.php @@ -1,894 +1,998 @@ - ['column' => 'idColumn', 'type' => 'int', 'not_null' => true, 'primary' => true, 'custom' => 'AUTO_INCREMENT'], - 'owner' => ['type' => 'int', 'foreign' => ['model' => UserModel::class, 'field' => 'id', 'on_delete' => 'cascade', 'on_update' => 'set null']] - ]*/ - - /** @var array */ - public const FOREIGNS = []; - - /** @var string */ - public const ID = 'id'; - - /** - * @var array - * @example ['id' => ['value' => 1, 'modified' => false]] - */ - protected $fields = []; - - /** - * Use wildcard in select queries - * - * @var boll - */ - public const WILDCARD = true; - - //Many be usefull but not recommended - public const FILTER = null; - public const INNER = null; - public const ORDER = null; - - - /*=== MAGIC ===*/ - /** - * Construct a model element with data - * - * @param array $data to fill $fields - * @param boolean $useColumns use Column's name or Field's name - */ - public function __construct(array $data = [], bool $useColumns = false){ - foreach (static::getFields() as $field => $options) { - $column = $useColumns ? static::getColumn($field) : $field; - $value = static::convertField(isset($data[$column]) ? $data[$column] : (isset($options['default']) ? $options['default'] : null), $field); - $this->fields[$field] = [ - 'value' => $value, - 'modified' => false - ]; - } - foreach(static::getForeigns() as $foreign => $options){ - $this->fields[$foreign] = [ - 'modified' => false - ]; - } - } - - //MAYBE: Save on destroy - - /** - * Get data without reflection (may became heavy) - * - * @param string $field value's key or foreign's key if start with _ - * @return mixed value, Model or null - */ - public function __get(string $field){ - if(strlen($field) > 0){ - if($field[0] == '_') - return $this->get(substr($field, 1), true); - - if(array_key_exists($field, $this->fields) && array_key_exists('value', $this->fields[$field])) - return $this->fields[$field]['value']; - } - - $trace = debug_backtrace(); - trigger_error( - 'Propriété non-définie via __get() : ' . $field . - ' dans ' . $trace[0]['file'] . - ' à la ligne ' . $trace[0]['line'], - E_USER_NOTICE); - return null; - } - - public function __isset(string $field): bool{ - if(strlen($field) > 0){ - if($field[0] == '_') - return $this->haveForeignOptions(substr($field, 1)); - - if(array_key_exists($field, $this->fields) && array_key_exists('value', $this->fields[$field])) - return true; - } - - return false; - } - - /** - * Store data in $fields - * - * @param string $field - * @param mixed $value - */ - public function __set(string $field, $value){ - if(array_key_exists($field, static::FIELDS)){ - $this->fields[$field] = [ - 'value' => static::convertField($value, $field), - 'modified' => true - ]; - }else{ - $trace = debug_backtrace(); - trigger_error( - 'Propriété non-définie via __set() : ' . $field . - ' dans ' . $trace[0]['file'] . - ' à la ligne ' . $trace[0]['line'], - E_USER_NOTICE); - } - } - - /** - * Set foreign's data - * - * @param string $field - * @param Model|array|null $data - * @return void - */ - public function set(string $field, $data){ - if(!is_a($data, Model::class) && !is_array($data) && $data !== null) - throw new DatabaseException('Set data must be a Model, array of Model or null'); - - if((array_key_exists($field, static::FIELDS) && isset(static::getOptions($field)['foreign'])) || array_key_exists($field, static::FOREIGNS)){ - $this->fields[$field]['foreign'] = $data; - }else{ - $trace = debug_backtrace(); - trigger_error( - 'Propriété non-définie via set() : ' . $field . - ' dans ' . $trace[0]['file'] . - ' à la ligne ' . $trace[0]['line'], - E_USER_NOTICE); - } - } - - /** - * Get foreign's data - * - * @param string $field - * @param boolean $load must load data if can't find it - * @return Model|array|null - */ - public function get(string $field, bool $load = false){ - if(array_key_exists($field, $this->fields)){ - if(array_key_exists('foreign', $this->fields[$field])) - return $this->fields[$field]['foreign']; - else{ - if($load){ - $foreign = static::getForeignOptions($field); - - if(!isset($foreign['model'])) - throw new DatabaseException('Any model for foreign in field '.$field); - - $model = $foreign['model']; - - if(!class_exists($model)) - throw new DatabaseException('Can\'t find class '.$model.' for foreign in field '.$field); - - $id = $this->{isset($foreign['for']) ? $foreign['for'] : $field}; - $where = '`'.$model::getColumn(isset($foreign['field']) ? $foreign['field'] : $model::ID).'` = ?'; - - $value = (isset($foreign['multiple']) && $foreign['multiple']) ? - $model::all([$id], $where): - $model::first([$id], $where); - - //MAYBE: Make nullable check - - $this->fields[$field]['foreign'] = $value; - return $value; - } - } - } - - $trace = debug_backtrace(); - trigger_error( - 'Propriété non-définie via get() : ' . $field . - ' dans ' . $trace[0]['file'] . - ' à la ligne ' . $trace[0]['line'], - E_USER_NOTICE); - return null; - } - - /** - * Just get foreign's data or return null - * - * @param string $field - * @return Model|array|null - */ - public function tryGet(string $field){ - if(array_key_exists($field, $this->fields)){ - if(array_key_exists('foreign', $this->fields[$field])) - return $this->fields[$field]['foreign']; - } - - return null; - } - - /*=== CREATE ===*/ - /** - * Create Model from PDOStatement - * - * @param \PDOStatement $row - * @param boolean $exception must throw exception ? - * @return self|null - */ - public static function fromRow(\PDOStatement $row, bool $exception = true): ?self{ - if($row->rowCount() < 1){ - if($exception) - throw new \Exception('Create from Any Row'); - return null; - } - - $data = $row->fetch(); - return new static($data, true); - } - - /** - * Create Model array from PDOStatement - * - * @param \PDOStatement $row - * @param boolean $exception must throw exception ? - * @return array - */ - public static function fromRowAll(\PDOStatement $row, bool $exception = true): array{ - if($row->rowCount() < 1){ - if($exception) - throw new \Exception('Create from Any Row'); - return []; - } - - $res = array(); - while($data = $row->fetch()){ - $res[] = new static($data, true); - } - return $res; - } - - /** - * Create Model array from data array - * - * @param array $data - * @param boolean $useColumns see __construct - * @return array - */ - public static function fromData(array $data, bool $useColumns = false): array{ - $res = array(); - foreach($data as $element){ - $res[] = new static($element, $useColumns); - } - return $res; - } - - - /*=== CONST ===*/ - /** - * Same as static::FIELDS with empty check - * - * @param boolean $exception - * @return array - */ - public static function getFields(bool $exception = true): array{ - if(empty(static::FIELDS) && $exception) - throw new DatabaseException('FIELDS not set'); - - return static::FIELDS; - } - - /** - * Get params for a specific field - * - * @param string $field - * @return array - */ - public static function getOptions(string $field): array{ - $fields = static::getFields(); - if(!isset($fields[$field])){ - if(array_key_exists($field, static::FOREIGNS)) - return static::FOREIGNS[$field]; - - throw new DatabaseException('Can\'t find field : '.$field); - } - - return $fields[$field]; - } - - /** - * Same as getFields for static::FOREIGNS - * - * @param boolean $exception - * @return array - */ - public static function getForeigns(bool $exception = false): array{ - if(empty(static::FOREIGNS) && $exception) - throw new DatabaseException('FOREIGNS not set'); - - return static::FOREIGNS; - } - - /** - * Get FIELDS foreign options or FOREIGNS options - * - * @param string $field - * @return array - */ - public static function getForeignOptions(string $field): array{ - $fields = static::getFields(); - if(isset($fields[$field]) && isset($fields[$field]['foreign'])) - return is_array($fields[$field]['foreign']) ? $fields[$field]['foreign'] : ['model' => $fields[$field]['foreign']]; - - $foreigns = static::getForeigns(); - if(!isset($foreigns[$field])) - throw new DatabaseException('Not a foreign field'); - - return is_array($foreigns[$field]) ? $foreigns[$field] : ['model' => $foreigns[$field], 'for' => static::ID]; - } - - /** - * Check in field have FIELDS foreign options or FOREIGNS options - * - * @param string $field - * @return bool - */ - public static function haveForeignOptions(string $field): bool{ - $fields = static::getFields(false); - if($field == null) - return false; - - if(isset($fields[$field]) && isset($fields[$field]['foreign'])) - return true; - - $foreigns = static::getForeigns(); - return isset($foreigns[$field]); - } - - /** - * Convert field to column - * - * @param string $field - * @param boolean $sql add table name and quote - * @return string - */ - public static function getColumn(string $field, bool $sql = false): string{ - $options = static::getOptions($field); - $column = isset($options['column']) ? $options['column'] : $field; - return $sql ? '`'.static::TABLE.'`.`'.$column.'`' : $column; - } - - /** - * Get table ID (for find and findOrFail) - * - * @param boolean $sql add table name and quote - * @return string - */ - public static function getID(bool $sql = false): string{ - return static::getColumn(static::ID, $sql); - } - - /** - * Get column's names - * - * @param boolean $sql add table name and quote - * @return array - */ - public static function getColumns(bool $sql = true): array{ - $fields = static::getFields(); - $columns = []; - foreach ($fields as $field => $options) { - $columns[] = static::getColumn($field, $sql); - } - return $columns; - } - - /** - * Same as getColumns but only with 'primary' => true fields - * - * @param boolean $sql add table name and quote - * @return array - */ - public static function getPrimaryColumns(bool $sql = true): array{ - $fields = static::getFields(); - $columns = []; - foreach ($fields as $field => $options) { - if(isset($options['primary']) && $options['primary']){ - $column = static::getColumn($field); - $columns[] = $sql ? '`'.static::TABLE.'`.`'.$column.'`' : $column; - } - } - return $columns; - } - - /** - * Same as getColumns but only with 'modified' => true $this->fields - * - * @param boolean $sql add table name and quote - * @return array - */ - public function getModifiedColumns(bool $sql = true): array{ - $fields = static::getFields(); - $columns = []; - foreach ($fields as $field => $options) { - if(isset($this->fields[$field]['modified']) && $this->fields[$field]['modified']){ - $column = static::getColumn($field); - $columns[] = $sql ? '`'.static::TABLE.'`.`'.$column.'`' : $column; - } - } - return $columns; - } - - /** - * Get fields values (reverse of static::fromData) - * - * @return array - */ - public function getValues(){ - $values = []; - foreach ($this->fields as $field => $data) { - if(array_key_exists('value', $data)) - $values[] = $data['value']; - } - return $values; - } - - /** - * Same as getValues but only with 'modified' => true $this->fields - * - * @return array - */ - public function getModifiedValues(){ - $values = []; - foreach ($this->fields as $field => $data) { - if(isset($data['modified']) && $data['modified']) - $values[] = $data['value']; - - } - return $values; - } - - /** - * Remove all modified flags - * - * @return void - */ - public function unmodify(){ - foreach($this->fields as $field => $data){ - if(isset($data['modified'])) - $this->fields[$field]['modified'] = false; - } - } - - /** - * Same as getValues but only with 'primary' => true fields - * - * @return array - */ - public function getPrimaryValues(){ - $values = []; - foreach ($this->fields as $field => $data) { - $options = static::getOptions($field); - if(isset($options['primary']) && $options['primary']) $values[] = $data['value']; - } - return $values; - } - - /** - * Convert input to sql valid value - * - * @param mixed $data - * @param string $field - * @return mixed - */ - protected static function convertField($data, string $field){ - $options = static::getOptions($field); - if(is_null($data)){ - if(isset($options['not_null']) && $options['not_null'] == true) - throw new DatabaseException('Can\'t set null to NOT NULL field : '.$field); - }else{ - if(isset($options['type'])){ - switch(strtolower($options['type'])){ - case 'int': - $data = intval($data); //MAYBE: E_NOTICE on strange types - break; - case 'char': - case 'varchar': - case 'text': - $data = strval($data); //MAYBE: E_NOTICE on strange types - if(isset($options['lenght']) && strlen($data) > $options['lenght']) - throw new DatabaseException('data is to long in field : '.$field); - break; - case 'bit': - $data = boolval($data); //MAYBE: E_NOTICE on strange types - break; - case 'date': - $data = (is_a($data, \DateTime::class) ? $data : new \DateTime(strval($data)))->format('Y-m-d'); - break; - case 'time': - $data = (is_a($data, \DateTime::class) ? $data : new \DateTime(strval($data)))->format('H:i:s'); - break; - case 'datetime': - $data = (is_a($data, \DateTime::class) ? $data : new \DateTime(strval($data)))->format('Y-m-d H:i:s'); - break; - default: - throw new DatabaseException('unknown type in field : '.$field); - break; - } - } - - return $data; - } - } - - - /*=== QUERIES ===*/ - /** - * Create Request\Select from Model - * - * @return Request\Select - */ - public static function select(): Request\Select{ - $req = Connection::get(static::DATABASE) - ->select()->from(static::TABLE); - - if(!static::WILDCARD) - $req = $req->fields(static::getColumns()); - - - if(static::INNER != null) - $req = $req->join(static::INNER); - - if(static::FILTER != null) - $req = $req->where(static::FILTER); - - if(static::ORDER != null) - $req = $req->orderby(static::ORDER); - - return static::prepare($req); - } - - /** - * Create Request\Insert (without data) from Model - * - * @return Request\Insert - */ - public static function insert(): Request\Insert{ - $req = Connection::get(static::DATABASE) - ->insert(static::getColumns()) - ->into(static::TABLE); - - return $req; - } - - /** - * add values to insert and run it - * - * @param boolean $forceID must update id value - */ - public function runInsert(bool $forceID = true){ - $insert = static::insert(); - $res = $insert->run($this->getValues()); - if($forceID && static::ID != null) - $this->{static::ID} = Connection::get(static::DATABASE)->getLastInsertID(); - - return $res; - } - - /** - * Change value of modified fields in database - * - * @return Model - */ - public function runUpdate(){ - $req = Connection::get(static::DATABASE) - ->update(static::getModifiedColumns()) - ->table(static::TABLE) - ->where(implode(' AND ', array_map(function($field){ return $field.' = ?'; }, static::getPrimaryColumns()))) - ->run(array_merge($this->getModifiedValues(true), $this->getPrimaryValues())); - $this->unmodify(); - return $this; - } - - /** - * Delete instance in database - */ - public function runDelete(){ - $req = Connection::get(static::DATABASE) - ->delete() - ->from(static::TABLE) - ->where(implode(' AND ', array_map(function($field){ return $field.' = ?'; }, static::getPrimaryColumns()))) - ->run($this->getPrimaryValues()); - } - - /** - * Create Request\Create from Model - * - * @return Request\Create - */ - public static function create(): Request\Create{ - $req = Connection::get(static::DATABASE) - ->create(static::TABLE); - - foreach (static::getFields() as $field => $options) { - $column = static::getColumn($field); - $req->column( - $column, - $options['type'], - isset($options['lenght']) ? $options['lenght'] : null, - isset($options['not_null']) && $options['not_null'], - isset($options['custom']) ? $options['custom'] : null); - - if(isset($options['primary']) && $options['primary']) - $req->primary($column); - - if(isset($options['unique']) && $options['unique']) - $req->unique($column); - - $index = false; - - if(isset($options['foreign'])){ - $foreign = $options['foreign']; - - $model = null; - - if(is_array($foreign)){ - if(!isset($foreign['model'])) - throw new DatabaseException('Any model for foreign in field '.$field); - - $model = $foreign['model']; - }else{ - $model = $foreign; - } - if(!class_exists($model)) - throw new DatabaseException('Can\'t find class '.$model.' for foreign in field '.$field); - - $req->foreign($column, $model::TABLE, - $model::getColumn(isset($foreign['field']) ? $foreign['field'] : $model::ID), - isset($foreign['on_delete']) ? $foreign['on_delete'] : null, - isset($foreign['on_update']) ? $foreign['on_update'] : null); - - $index = true; - } - - if(isset($options['index']) ? $options['index'] : $index) - $req->index($column); - } - - return $req; - } - - /** - * Create Request\Drop from Model - * - * @return Request\Drop - */ - public static function drop(): Request\Drop{ - return Connection::get(static::DATABASE) - ->drop(static::TABLE); - } - - /* Do advanced customuzation here */ - /** - * Modify default select request - * - * @param Request\Select $req - * @return Request\Select - */ - protected static function prepare(Request\Select $req): Request\Select{ return $req; } - - /** - * Really used only for next functions - * - * @param array $values - * @param string $where - * @return PDOStatement - */ - public static function runSelect(array $values = null, string $where = null): \PDOStatement{ - $req = static::select(); - - if(isset($where)) - $req = $req->where($where, true); - - return $req->run($values); - } - - /** - * Get first row than match $where with $values or null - * - * @param array $values - * @param string $where - * @return self|null - */ - public static function first(array $values = null, string $where = null): ?self{ - return static::fromRow(static::runSelect($values, $where), false); - } - - /** - * Same as first but throw exception on null - * - * @param array $values - * @param string $where - * @return self - */ - public static function firstOrFail(array $values = null, string $where = null): self{ - return static::fromRow(static::runSelect($values, $where)); - } - - /** - * Get all rows than match $where with $values (may be empty) - * - * @param array $values - * @param string $where - * @return array - */ - public static function all(array $values = null, string $where = null): array{ - return static::fromRowAll(static::runSelect($values, $where), false); - } - - /** - * Same as all but throw exception on empty - * - * @param array $values - * @param string $where - * @return array - */ - public static function allOrFail(array $values = null, string $where = null): array{ - return static::fromRowAll(static::runSelect($values, $where)); - } - - /** - * Check if at least one row exists - * - * @param array $values - * @param string $where - * @return boolean - */ - public static function exists(array $values = null, string $where = null): bool{ - return static::first($values, $where) !== null; - } - - /** - * Count row than match $where with $values - * - * @param array $values - * @param string $where - * @return integer - */ - public static function count(array $values = null, string $where = null): int{ - $req = static::select(); - $req->fields(['COUNT(*) as count']); - - if(isset($where)) - $req = $req->where($where, true); - - $data = $req->run($values)->fetch(); - if(!isset($data['count'])) - return 0; - - return $data['count']; - } - - /** - * Use static:ID to get row - * - * @param mixed $id int is a good idea - * @return self|null - */ - public static function find($id): ?self{ - return static::first(array($id), static::getID().' = ?'); - } - - /** - * Same as find but throw exception on null - * - * @param mixed $id int is a good idea - * @return self - */ - public static function findOrFail($id): self{ - return static::firstOrFail(array($id), static::getID().' = ?'); - } - - /** - * Use static:ID to get rows - * - * @param array $ids array(int) is a good idea - * @return array|null - */ - public static function finds(array $ids): ?array{ - return static::all($ids, static::getID().' IN ( '.str_repeat('?, ', count($ids)-1).'? )'); - } - - /** - * Same as find but throw exception on null - * - * @param array $ids array(int) is a good idea - * @return array - */ - public static function findsOrFail(array $ids): array{ - return static::allOrFail($ids, static::getID().' IN ( '.str_repeat('?, ', count($ids)-1).'? )'); - } - - /** - * Preload foreign Model for a group of Models - * - * @param array $models - * @param string $field - * @return array updated models - */ - public static function load(array $models, string $field): array{ - if(!empty($models)){ - $foreign = static::getForeignOptions($field); - - if(!isset($foreign['model'])) - throw new DatabaseException('Any model for foreign in field '.$field); - - $model = $foreign['model']; - - if(!class_exists($model)) - throw new DatabaseException('Can\'t find class '.$model.' for foreign in field '.$field); - - $ids = []; - foreach ($models as $current) { - $ids[] = $current->{isset($foreign['for']) ? $foreign['for'] : $field}; - } - $ids = array_values(array_unique($ids)); - $foreigns = []; - foreach($model::all($ids, $model::getColumn(isset($foreign['field']) ? $foreign['field'] : $model::ID).' IN ( '.str_repeat('?, ', count($ids)-1).'? )') as $current){ - $cid = $current->{isset($foreign['field']) ? $foreign['field'] : $model::ID}; - if(isset($foreign['multiple']) && $foreign['multiple']) - $foreigns[$cid][] = $current; - else - $foreigns[$cid] = $current; - } - - foreach ($models as &$current) { - $id = $current->{isset($foreign['for']) ? $foreign['for'] : $field}; - if(isset($foreigns[$id])) - $current->set($field, $foreigns[$id]); - else if(!isset($foreign['nullable']) || !$foreign['nullable']) - var_dump($ids);//throw new DatabaseException('Null foreign model'); - else - $current->set($field, null); - } - } - - return $models; - } - - /** - * Preload multiple foreign Model with recursivity for a group of Models - * - * @param array $models - * @param array $fields - * @return array updated models - */ - public static function loads(array $models, array $fields): array{ - foreach($fields as $field => $data){ - $subfields = []; - if(is_array($data)){ - $subfields = $data; - }else{ - $field = $data; - } - $models = static::load($models, $field); - if(!empty($subfields)){ - $submodels = []; - foreach($models as $model){ - if($model->tryGet($field) != null) - $submodels[] = $model->get($field); - } - if(!empty($submodels)){ - $submodels[0]::loads($submodels, $subfields); - } - } - } - return $models; - } + [ + 'convert' => 'intval' + ], + 'integer' => 'int', + 'bigint' => 'int', + 'smallint' => 'int', + 'bool' => [ + 'data_type' => 'bit', + 'convert' => 'boolval' + ], + 'boolean' => 'bool', + 'varchar' => [ + 'length' => 'strlen', + 'convert' => 'strval' + ], + 'date' => [ + 'convert' => 'Krutush\Database\TypeHelper::dateConvert' + ], + 'time' => [ + 'convert' => 'Krutush\Database\TypeHelper::timeConvert' + ], + 'datetime' => [ + 'convert' => 'Krutush\Database\TypeHelper::datetimeConvert' + ], + 'timestamp' => 'datetime' + ]; + + public static function getDataType(string $type): array{ + $data = static::DATA_TYPES[$type] ?? ['convert' => 'strval']; + return is_string($data) ? static::getDataType($data) : $data; + } + + /** + * Use wildcard in select queries + * + * @var bool + */ + public const WILDCARD = true; + + /** @var string */ + public const DATABASE = null; + + /** + * Get database name for static::DATABASE (useless: just in case, we became smarter in the future) + * + * @return string + */ + public static function getDatabase(): ?string{ + return static::DATABASE; + } + + + /** @var string */ + public const TABLE = null; + + /** @var string */ + public const CLASS_PREFIX = 'Model\\'; + + /** @var string */ + public const CLASS_SUFFIX = 'Model'; + + /** + * Get table name for static::TABLE or class name + * + * @example \Exemple\UserModel => exemple_user and \...\Model\CarFactory\Wheel => car_factory_wheel + * @return string + */ + public static function getTable(): string{ + if(static::TABLE != null) + return static::TABLE; + + $table = static::class; + if($prefix = strpos($table, static::CLASS_PREFIX)) + $table = substr($table, $prefix+strlen(static::CLASS_PREFIX)); //Cut class full name with CLASS_PREFIX + + if($suffix = strrpos($table, static::CLASS_SUFFIX, strrpos($table, '\\'))) + $table = substr($table, 0, $suffix); //Cut with CLASS_SUFFIX + + return strtolower(str_replace('\\', '', preg_replace('/(? string (name in database) + 'type' => string (type in model) + 'data_type' => string (type in database) + 'length' => int (used with data_type) + 'unique' => bool|string|int|[] (define unique constraints) + 'index' => bool|string|int|[] (define indexs) + 'not_null' => bool (enable NOT NULL) + 'primary' => bool (define primary key constraint) + 'foreign' => string|[ (define foreign key) + 'constraint' => bool (create constraint in database) + 'model' => class (class to link) + 'key' => string (field to link) + 'to' => string (field linked in model) + 'multiple' => bool (allow 0-n links) + 'on_update' => string (constraint property) + 'on_delete' => string (constraint property) + ] + 'default' => mixed (default value) + 'virtual' => bool (field not prevent in database: internal data like reverse foreign key) + 'custom' => raw sql create data + */ + + /** @var array */ + protected static $complete_fields = null; + + /** + * Get static::FIELDS but better (used in Model exteneds) + * + * @return array + */ + public static function getFields(): array{ + if(empty(static::FIELDS)) + throw new \UnexpectedValueException('static::FIELDS is empty'); + + if(!isset(static::$complete_fields)) + static::$complete_fields = static::completeFields(static::FIELDS); + + return static::$complete_fields; + } + + + /*=== MAGIC ===*/ + /** + * Construct a model element with data + * + * @param array $data to fill $fields + * @param boolean $useColumns use Column's name or Field's name + */ + public function __construct(array $data = [], bool $useColumns = false){ + foreach (static::getFields() as $field => $options) { + $this->fields[$field] = [ + 'value' => static::convertField($data[$useColumns ? static::getColumn($field, $options) : $field] ?? $options['default'] ?? null, $field, $options), + 'modified' => false + ]; + } + } + + /** + * Get field value + * + * @param string $field value's key or foreign's key if start with _ + * @return mixed value, Model or null + */ + public function __get(string $field){ + if(strlen($field) > 0){ + if($field[0] == '_') + return static::loadForeign(substr($field, 1)); + + if(array_key_exists($field, $this->fields) && array_key_exists('value', $this->fields[$field])) + return $this->fields[$field]['value']; + } + + trigger_error('Unknown property in __get() : "'.$field.'"'); + return null; + } + + public function __isset(string $field): bool{ + if(strlen($field) > 0){ + if($field[0] == '_') + return static::haveForeign(substr($field, 1)); + + if(array_key_exists($field, $this->fields) && array_key_exists('value', $this->fields[$field])) + return true; + } + + return false; + } + + /** + * Store data in $fields + * + * @param string $field + * @param mixed $value + */ + public function __set(string $field, $value){ + if(strlen($field) > 0){ + if($field[0] == '_') + return static::saveForeign(substr($field, 1)); + + if(array_key_exists($field, static::FIELDS)){ + $this->fields[$field] = [ + 'value' => static::convertField($value, $field, static::getOptions($field)), + 'modified' => true + ]; + return; + } + } + trigger_error('Unknown property in __set() : "'.$field.'"'); + } + + + /*=== QUERIES ===*/ + /** + * Create Request\Select from Model + * + * @return Request\Select + */ + public static function getSelect(): Request\Select{ + $req = Connection::get(static::getDatabase()) + ->select()->from(static::getTable()); + + if(!static::WILDCARD) + $req = $req->fields(static::getColumns(false)); + + return $req; + } + + /** + * Really used only for next functions + * + * @param array $values + * @param string $where + * @return PDOStatement + */ + public static function select(array $values = null, string $where = null): \PDOStatement{ + $req = static::getSelect(); + + if(isset($where)) + $req = $req->where($where, true); + + return $req->run($values); + } + + /** + * Create Request\Insert (without data) from Model + * + * @return Request\Insert + */ + public static function getInsert(): Request\Insert{ + return Connection::get(static::getDatabase()) + ->insert(static::getColumns(false)) + ->into(static::getTable()); + } + + /** + * add values to static::getInsert and run it + */ + public function insert(){ + $req = static::getInsert()->run($this->getColumnValues(false)); + $this->unmodify(); + return $req; + } + + /** + * Create Request\Update (without data) from Model + * + * @return Request\Update + */ + public static function getUpdate(): Request\Update{ + return Connection::get(static::getDatabase()) + ->update()->into(static::getTable()); + } + + /** + * Change value of modified fields in database + * + * @return Model + */ + public function update(){ + static::getUpdate() + ->fields(static::getModifiedColumns(false)) + ->where(static::getPrimarySelector()) + ->run(array_merge($this->getModifiedValues(true), $this->getPrimaryValues())); + $this->unmodify(); + return $this; + } + + protected static function getPrimarySelector(): string{ + return Request::combineParams( + array_map(function($options){ + return Request::toParam($options['column']); + }, array_filter(function($options){ + return !$options['virtual'] && $options['primary']; + }, static::getFields())) + ); + } + + /** + * update or insert if primary null or modified exsits + */ + public function save(){ + foreach(static::getFields() as $field => $options){ + if(!$options['virtual'] && $options['primary'] && ($this->fields[$field]['value'] === null || $this->fields[$field]['modified'])) + return static::insert(); + } + //MAYBE: check exists + return static::update(); + } + + /** + * Create Request\Delete for modified columns (without data) from Model + * + * @return Request\Delete + */ + public static function getDelete(): Request\Delete{ + return Connection::get(static::getDatabase()) + ->delete()->from(static::getTable()); + } + + /** + * Delete instance in database + */ + public function delete(){ + return static::getDelete() + ->where(static::getPrimarySelector()) + ->run($this->getPrimaryValues()); + } + + /** + * Create Request\Create from Model + * + * @return Request\Create + */ + public static function getCreate(): Request\Create{ + $req = Connection::get(static::getDatabase()) + ->create(static::getTable()); + + $uniques = []; + $indexs = []; + foreach (static::getFields() as $field => $options) { + if(!$options['virtual']){ + $req->column($options['column'], $options['data_type'], $options['lenght'], $options['not_null'], $options['custom']); + + if($options['primary']) + $req->primary($column); + + foreach($options['unique'] as $unique){ + $uniques[is_bool($unique) ? $options['column'] : $unique][] = $options['column']; + } + + foreach($options['index'] as $index){ + $indexs[is_bool($index) ? $options['column'] : $unique][] = $options['column']; + } + } + if(static::haveForeign($field, $options) && $options['foreign']['constraint']){ + //TODO: multiple + $req->foreign($options['foreign']['key'], + $options['foreign']['model']::getTable(), + $options['foreign']['model']::getColumn($options['foreign']['to']), + $options['foreign']['on_delete'], + $options['foreign']['on_update']); + } + } + + foreach($uniques as $uq_id => $columns){ + if(is_int($uq_id)) + $uq_id = implode('_', $columns); + + $req->unique($uq_id, $columns); + } + + foreach($indexs as $id_id => $columns){ + if(is_int($id_id)) + $id_id = implode('_', $columns); + + $req->index($id_id, $columns); + } + + return $req; + } + + /** + * Create Request\Drop from Model + * + * @return Request\Drop + */ + public static function getDrop(): Request\Drop{ + return Connection::get(static::getDatabase()) + ->drop(static::getTable()); + } + + /** + * Get first row than match $where with $values or null + * + * @param array $values + * @param string $where + * @param array $loads run static::load + * @return self|null + */ + public static function first(array $values = null, string $where = null, array $loads = []): ?self{ //MAYBE: Limit 1 + $res = static::fromRow(static::select($values, $where), false); + if($res === null) + return null; + + return static::load($res, $loads); + } + + /** + * Same as first but throw exception on null + * + * @param array $values + * @param string $where + * @param array $loads run static::load + * @return self + */ + public static function firstOrFail(array $values = null, string $where = null, array $loads = []): self{ + return static::load(static::fromRow(static::select($values, $where)), $loads); + } + + /** + * Get all rows than match $where with $values (may be empty) + * + * @param array $values + * @param string $where + * @param array $loads run static::loads + * @return array + */ + public static function all(array $values = null, string $where = null, array $loads = []): array{ + return static::loads(static::fromRowAll(static::select($values, $where), false), $loads); + } + + /** + * Same as all but throw exception on empty + * + * @param array $values + * @param string $where + * @param array $loads run static::loads + * @return array + */ + public static function allOrFail(array $values = null, string $where = null, array $loads = []): array{ + return static::loads(static::fromRowAll(static::select($values, $where)), $loads); + } + + /** + * Check if at least one row exists + * + * @param array $values + * @param string $where + * @return boolean + */ + public static function exists(array $values = null, string $where = null): bool{ + return static::first($values, $where) !== null; + } + + /** + * Count row than match $where with $values + * + * @param array $values + * @param string $where + * @return integer + */ + public static function count(array $values = null, string $where = null): int{ + $req = static::getSelect(); + $req->fields(['COUNT(*) as count']); //Hope than db simply as count() + + if(isset($where)) + $req = $req->where($where, true); + + $data = $req->run($values)->fetch(); + if(!isset($data['count'])) + return 0; + + return $data['count']; + } + + /*=== UTILITIES ===*/ + /** + * Get options for a specifific field + * + * @param string $field Field name + * @return array Options + */ + public static function getOptions(string $field): array{ + $fields = static::getFields(); + if(!array_key_exists($field, $fields)) + throw new \InvalidArgumentException('Can\'t find field : "'.$field.'"'); + return $fields[$field]; + } + + /** + * Get column name from field + * + * @param string $field Field name + * @return string Column name + */ + public static function getColumn(string $field, array $options = null): string{ + return ($options ?? static::getOptions($field))['column']; + } + + /** + * Get column names + * + * @param bool $virtual + * @return array Column names + */ + public static function getColumns(bool $virtual = null): array{ + $columns = []; + foreach(static::getFields() as $field => $options){ + if($virtual === null || $options['virtual'] == $virtual) + $columns[] = $options['column']; + } + return $columns; + } + + /** + * Get modified column names + * + * @param bool $virtual + * @return array Column names + */ + public function getModifiedColumns(bool $virtual = null): array{ + $columns = []; + foreach(static::getFields() as $field => $options){ + if(($virtual === null || $options['virtual'] == $virtual) && $this->fields[$field]['modified']) + $columns[] = $options['column']; + } + return $columns; + } + + /** + * Get column values + * + * @param bool $virtual + * @param bool $modified + * @return array Column names + */ + public function getColumnValues(bool $virtual = null, bool $modified = null): array{ + $values = []; + foreach(static::getFields() as $field => $options){ + if(($virtual === null || $options['virtual'] == $virtual) && ($modified === null || $this->fields[$field]['modified'] == $modified)) + $values[] = $this->fields[$field]['value']; + } + return $values; + } + + protected function getPrimaryValues(): array{ + $values = []; + foreach(static::getFields() as $field => $options){ + if(!$options['virtual'] && $options['primary']) + $values[] = $this->fields[$field]['value']; + } + return $values; + } + + /** + * Unset modified flags + */ + public function unmodify(){ + foreach($this->fields as $field => $data){ + $this->fields[$field]['modified'] = false; + } + } + + /** + * static::FIELDS[$field] with some checks + * + * @param string $field + * @return array options + */ + protected static function getFieldRawOptions(string $field): array{ + if(isset(static::FIELDS[$field])) + return static::FIELDS[$field]; + + if(!in_array($field, static::FIELDS)) + throw new \InvalidArgumentException('Unknown field : "'.$field.'"'); + + return []; + } + + /** + * Add all missing options to FIELDS + * + * @param array $input FIELDS + * @return array completed fields + */ + protected static function completeFields(array $input): array{ + $havePrimary = false; + $fields = []; + foreach ($input as $field => $data) { + if(is_int($field)){ + $field = $data; + $data = []; + } + + if(!is_string($field)) + throw new \UnexpectedValueException('Field name must be a string'); + + if(array_key_exists($field, $fields)) + throw new \UnexpectedValueException('Duplicate field name : "'.$field.'"'); + + if(!is_array($data)) + $data = ['column' => $data]; + + $options = []; + $options['column'] = $data['column'] ?? $field; + if(!is_string($options['column'])) + throw new \UnexpectedValueException('Column name must be a string : "'.$field.'"'); + + $options['virtual'] = $data['virtual'] ?? false; + if(!is_bool($options['virtual'])) + throw new \UnexpectedValueException('Field virtual option must be a bool : "'.$field.'"'); + + if(isset($data['foreign'])){ + $options['foreign']['constraint'] = $data['foreign']['constraint'] ?? $options['virtual']; + if(!is_bool($options['foreign']['constraint'])) + throw new \UnexpectedValueException('Field contraint of foreign must be a bool : "'.$field.'"'); + + $options['foreign']['model'] = is_string($data['foreign']) ? $data['foreign'] : ($data['foreign']['model'] ?? static::class); + if(!is_subclass_of($options['foreign']['model'], self::class)) + throw new \UnexpectedValueException('Field model of foreign must be a subclass of "'.self::class.'" : "'.$field.'"'); + + $options['foreign']['key'] = $data['foreign']['key'] ?? ($options['virtual'] ? null : $field); + if(!is_string($options['foreign']['key'])) + throw new \UnexpectedValueException('Field key of foreign must be a string : "'.$field.'"'); + + $options['foreign']['to'] = $data['foreign']['to'] ?? call_user_func([$options['foreign']['model'], 'getID']); + if(!is_string($options['foreign']['to'])) + throw new \UnexpectedValueException('Field to of foreign must be a string : "'.$field.'"'); + + $options['foreign']['multiple'] = $data['foreign']['multiple'] ?? false; + if(!is_bool($options['foreign']['multiple'])) + throw new \UnexpectedValueException('Field multiple of foreign must be a bool : "'.$field.'"'); + + $options['foreign']['on_update'] = $data['foreign']['on_update'] ?? ''; + if(!is_string($options['foreign']['on_update'])) + throw new \UnexpectedValueException('Field on_update of foreign must be a string : "'.$field.'"'); + + $options['foreign']['on_delete'] = $data['foreign']['on_delete'] ?? ''; + if(!is_string($options['foreign']['on_delete'])) + throw new \UnexpectedValueException('Field on_delete of foreign must be a string : "'.$field.'"'); + }else{ + $options['foreign'] = null; + } + + $f_options = isset($options['foreign']['model']) ? (call_user_func([$options['foreign']['model'], 'getFieldRawOptions'], $options['foreign']['to'])) : []; + + $options['type'] = $data['type'] ?? $f_options['type'] ?? 'varchar'; + if(!is_string($options['type'])) + throw new \UnexpectedValueException('Field type must be a string : "'.$field.'"'); + + if(!isset(static::DATA_TYPES[$options['type']])) + trigger_error('Unknown data type : "'.$options['type'].'" : "'.$field.'"'); + + $typeOptions = static::getDataType($options['type']); + + $options['data_type'] = $data['data_type'] ?? $f_options['data_type'] ?? $typeOptions['data_type'] ?? $options['type']; + if(!is_string($options['data_type'])) + throw new \UnexpectedValueException('Field data_type must be a string : "'.$field.'"'); + + if($options['data_type'] != ($f_options['data_type'] ?? $options['data_type'])) + trigger_error('Data type doesn\'t match foreign model data type : "'.$field.'"'); + + $options['length'] = $data['length'] ?? $f_options['length'] ?? (isset($typeOptions['length']) ? 50 : null); + if($options['length'] != null && !is_int($options['length'])) + throw new \UnexpectedValueException('Field length must be int or null : "'.$field.'"'); + + if($options['length'] != ($f_options['length'] ?? $options['length'])) + trigger_error('Data length doesn\'t match foreign model data length : "'.$field.'"'); + + $options['primary'] = $data['primary'] ?? false; + if(!is_bool($options['primary'])) + throw new \UnexpectedValueException('Field primary option must be a bool : "'.$field.'"'); + $havePrimary |= $options['primary']; + + $uniques = $data['unique'] ?? []; + if(!is_array($uniques)) + $uniques = [$uniques]; + + $options['unique'] = []; + foreach($uniques as $unique){ + if($unique === false) + continue; + + if($unique !== true && !is_int($unique) && !is_string($unique)) + throw new \UnexpectedValueException('Field unique option must be bool, int, string or array of : "'.$field.'"'); + $options['unique'][] = $unique; + } + $options['unique'] = array_values(array_unique($options['unique'])); + + $indexs = $data['index'] ?? []; + if(!is_array($indexs)) + $indexs = [$indexs]; + + $options['index'] = []; + $auto_index = isset($options['foreign']); + foreach($indexs as $index){ + if($index === false){ + $auto_index = false; + continue; + } + + if($index !== true && !is_int($index) && !is_string($index)) + throw new \UnexpectedValueException('Field index option must be bool, int, string or array of : "'.$field.'"'); + $options['index'][] = $index; + } + if($auto_index) + $options['index'][] = true; + $options['index'] = array_values(array_unique($options['index'])); + + + $options['not_null'] = $data['not_null'] ?? false; + if(!is_bool($options['not_null'])) + throw new \UnexpectedValueException('Field not_null option must be a bool : "'.$field.'"'); + + $options['default'] = $data['default'] ?? null; + + //TODO: increment, ... + + $options['custom'] = $data['custom'] ?? ''; + if(!is_string($options['custom'])) + throw new \UnexpectedValueException('Field custom option must be a string : "'.$field.'"'); + $fields[$field] = $options; + } + if(!$havePrimary) + trigger_error('Any primary key'); + return $fields; + } + + /** + * Convert data using DATA_TYPES + * + * @param mixed $data input value + * @param string $field field name for debug + * @param array $options containt type and length options + * @return mixed clean value + */ + protected static function convertField($data, string $field, array $options){ + if(is_null($data)){ + if($options['not_null']) + throw new \InvalidArgumentException('Can\'t set null to NOT NULL field : "'.$field.'"'); + }else{ + $typeOptions = static::getDataType($options['type']); + $data = call_user_func($typeOptions['convert'], $data); + + if(isset($typeOptions['length'])) + if($typeOptions['length']($data) > $options['length']) + throw new \InvalidArgumentException('Too long data in field : "'.$field.'"'); + + return $data; + } + } + + /** + * Check if field is foreign + * + * @param string $field + * @param array $options + * @return boolean + */ + public function haveForeign(string $field, array $options = null): bool{ + try{ + return isset(($options ?? static::getOptions($field))['foreign']); + }catch(\Exception $e){ + return false; + } + } + + public function isLoadedForeign(string $field): bool{ + return array_key_exists($field, $this->fields) && array_key_exists('foreign', $this->fields[$field]); + } + + /** + * Autoload foreign model + * + * @param string $field + * @param bool $reload + * @return null|Model|array + */ + public function loadForeign(string $field, bool $reload = false){ + if(!$reload && $this->isLoadedForeign($field)) + return $this->fields[$field]['foreign']; + + $options = static::getOptions($field); + if(!static::haveForeign($field, $options)) + throw new \InvalidArgumentException('Field is not a foreign key : "'.$field.'"'); + + $model = $options['foreign']['model']; + $key = $options['foreign']['key']; + + $where = Request::toParam($model::getColumn($options['foreign']['to'])); + $value = $options['foreign']['multiple'] ? + $model::all([$this->{$key}], $where): + $model::first([$this->{$key}], $where); + + //MAYBE: Make nullable check + + $this->saveForeign($field, $value, false, $options); + return $value; + } + + /** + * Autoload foreign model for multiple models + * + * @param array $models + * @param string $field + * @param bool $reload + * @return array values + */ + public static function loadsForeign(array $models, string $field, bool $reload = false){ //TODO: multiload + if(!empty($models)){ + $options = static::getOptions($field); + if(!static::haveForeign($field, $options)) + throw new \InvalidArgumentException('Field is not a foreign key : "'.$field.'"'); + + $model = $options['foreign']['model']; + $key = $options['foreign']['key']; + $to = $options['foreign']['to']; + $multiple = $options['foreign']['multiple']; + + $values = []; + if(!$reload){ + foreach($models as $model){ + $id = $model->{$key}; + if($model->isLoadedForeign($field) && !array_key_exists($id, $values)) + $values[$id] = $model->loadForeign($field); + } + $toload = []; + foreach($models as $model){ + if(!$model->isLoadedForeign($field)){ + $id =$model->{$key}; + if(array_key_exists($id, $values)) + $model->saveForeign($field, $values[$id], false, $options); + else + $toload[] = $model; + } + } + $models = $toload; + } + + $ids = []; + foreach ($models as $current) { + $ids[] = $current->{$key}; + } + $ids = array_values(array_unique($ids)); + + foreach($model::all($ids, Request::inParams($model::getColumn($to), $ids)) as $current){ + $cid = $current->{$to}; + if($multiple) + $values[$cid][] = $current; + else + $values[$cid] = $current; + } + + //MAYBE: Make nullable check + foreach($models as $model){ + if($reload || !$model->isLoadedForeign($field)) + $model->saveForeign($field, $values[$model->{$key}] ?? null, false, $options); + } + + $res = []; + foreach($values as $key => $value){ + array_push($res, $value); + } + return array_values(array_unique($res)); + } + } + + /** + * Recursivly autoload foreign models + * + * @param self $model + * @param array $loads + * @return Model update model + */ + public static function load(self $model, array $loads): self{ + foreach($loads as $field => $data){ + $subfields = []; + if(is_array($data)){ + $subfields = $data; + }else{ + $field = $data; + } + $loaded = $model->loadForeign($field); + if(!empty($subfields) && $loaded !== null){ + if(is_array($loaded)){ + $loaded[0]::loads($loaded, $subfields); + }else{ + $loaded::load($loaded, $subfields); + } + } + } + return $model; + } + + /** + * Recursivly autoload foreign models for multiple models + * + * @param array $models + * @param array $loads + * @return array updated models + */ + public static function loads(array $models, array $loads): array{ + foreach($loads as $field => $data){ + $subfields = []; + if(is_array($data)){ + $subfields = $data; + }else{ + $field = $data; + } + $loaded = static::loadsForeign($models, $field); + if(!empty($subfields)) + $loaded[0]::loads($loaded, $subfields); + } + return $models; + } + + public function saveForeign(string $field, $data, bool $updateKey = false, array $options = null){ + if(!is_a($data, static::class) && !is_array($data) && $data !== null) + throw new \InvalidArgumentException('Set data must be a Model, array of Model or null'); + + $options = $options ?: static::getOptions($field); + if(!static::haveForeign($field, $options)) + throw new \InvalidArgumentException('Field is not a foreign key : "'.$field.'"'); + + if($options['foreign']['multiple'] && !is_array($data)){ + if($data === null) + $data = []; + else + $data = [$data]; + } + + $this->fields[$field]['foreign'] = $data; + + if($updateKey){ + if($data === null) + $this->{$field} = null; + elseif(is_a($data, static::class)) + $this->{$field} = $data->{$options['foreign']['to']}; + } + } + + /** + * Create Model from PDOStatement + * + * @param \PDOStatement $row + * @param boolean $exception must throw exception ? + * @return self|null + */ + public static function fromRow(\PDOStatement $row, bool $exception = true): ?self{ + if($row->rowCount() < 1){ + if($exception) + throw new \InvalidArgumentException('Create from Any Row'); + return null; + } + + $data = $row->fetch(); + return new static($data, true); + } + + /** + * Create Model array from PDOStatement + * + * @param \PDOStatement $row + * @param boolean $exception must throw exception ? + * @return array + */ + public static function fromRowAll(\PDOStatement $row, bool $exception = true): array{ + if($row->rowCount() < 1){ + if($exception) + throw new \InvalidArgumentException('Create from Any Row'); + return []; + } + + $res = []; + while($data = $row->fetch()){ + $res[] = new static($data, true); + } + return $res; + } + + /** + * Get fields values (reverse of static::__construct) + * + * @return array + */ + public function getValues(): array{ + return array_map(function($data){ return $data['value']; }, $this->fields); + } } \ No newline at end of file diff --git a/src/ModelID.php b/src/ModelID.php new file mode 100644 index 0000000..b533610 --- /dev/null +++ b/src/ModelID.php @@ -0,0 +1,115 @@ + 'int', + 'primary' => true, + 'not_null' => true, + 'custom' => 'AUTO_INCREMENT' //TODO: be smart + ], + static::FIELDS[$field] ?? [] + ); + + return parent::getFieldRawOptions($field); + } + + /** + * add values to static::getInsert and run it + * + * @param boolean $setID must update id value + */ + public function insert(bool $setID = true){ + $res = parent::insert(); + if($setID) + $this->{static::getID()} = Connection::get(static::getDatabase())->getLastInsertID(); + + return $res; + } + + /** + * Use static:ID to get row + * + * @param mixed $id int is a good idea + * @param array $loads run static::load + * @return self|null + */ + public static function find($id, array $loads = []): ?self{ + return static::first([$id], Request::toParam(static::getID()), $loads); + } + + /** + * Same as find but throw exception on null + * + * @param mixed $id int is a good idea + * @param array $loads run static::load + * @return self + */ + public static function findOrFail($id, array $loads = []): self{ + return static::firstOrFail([$id], Request::toParam(static::getID()), $loads); + } + + /** + * Use static:ID to get rows + * + * @param array $ids array(int) is a good idea + * @param array $loads run static::loads + * @return array + */ + public static function finds(array $ids, array $loads = []): array{ + return static::all($ids, Request::inParams(static::getID(), count($ids)), $loads); + } + + /** + * Same as find but throw exception on empty + * + * @param array $ids array(int) is a good idea + * @param array $loads run static::loads + * @return array + */ + public static function findsOrFail(array $ids, array $loads = []): array{ + return static::allOrFail($ids, Request::inParams(static::getID(), count($ids)), $loads); + } +} \ No newline at end of file diff --git a/src/Request/Create.php b/src/Request/Create.php index c7188df..9879f45 100644 --- a/src/Request/Create.php +++ b/src/Request/Create.php @@ -1,97 +1,92 @@ -table = $table; - return $this; - } - - public function column(string $name, string $type, int $lenght = null, bool $not_null = false, string $more = null): Create{ //Really ? - $this->columns[] = '`'.$name.'` '.$type.($lenght ? '('.$lenght.')' : '').($not_null ? ' NOT NULL' : '').(isset($more) ? ' '.$more : ''); - return $this; - } - - public function primary(string $name): Create{ - $this->primary[] = '`'.$name.'`'; - return $this; - } - - public function unique(string $name): Create{ - $this->unique[$name] = ['`'.$name.'`']; - return $this; - } - - public function uniques(string $name, array $columns): Create{ - $this->unique[$name] = [$columns]; - return $this; - } - - public function index(string $name): Create{ - $this->index[$name] = ['`'.$name.'`']; - return $this; - } - - public function indexs(string $name, array $columns): Create{ - $this->index[$name] = [$columns]; - return $this; - } - - public function foreign(string $name, string $table, string $column, string $on_delete = null, string $on_update = null): Create{ //TODO: complex foreign - $this->foreign[$name] = compact('name', 'table', 'column', 'on_delete', 'on_update'); - return $this; - } - - public function sql(){ - if(!isset($this->table)) - throw new DatabaseException('Any table set'); - - if(empty($this->columns)) - throw new DatabaseException('Any columns set'); - - $uniques = []; - foreach ($this->unique as $name => $columns) { - $uniques[] = 'CONSTRAINT `UQ_'.ucfirst(strtolower($this->table)).'_'.ucfirst(strtolower($name)).'` UNIQUE ('.implode(', ', $columns).')'; - } - - $indexs = []; - foreach ($this->index as $name => $columns) { - $indexs[] = 'INDEX `ID_'.ucfirst(strtolower($this->table)).'_'.ucfirst(strtolower($name)).'` ('.implode(', ', $columns).')'; - } - - $foreigns = []; - foreach ($this->foreign as $name => $options) { - $foreigns[] = 'CONSTRAINT `FK_'.ucfirst(strtolower($this->table)).'_'.ucfirst(strtolower($name)).'` FOREIGN KEY (`'.$options['name'].'`) REFERENCES `'.$options['table'].'` (`'.$options['column'].'`)'. - (isset($options['on_delete']) ? 'ON DELETE '.$options['on_delete'] : ''). - (isset($options['on_update']) ? 'ON UPDATE '.$options['on_update'] : ''); - } - - return 'CREATE TABLE `'.$this->table.'`('."\n". - $sql = implode(",\n", - array_merge( - $this->columns, - (empty($this->primary) ? [] : [ - 'CONSTRAINT `PK_'.ucfirst(strtolower(strtok($this->table, ' '))).'` PRIMARY KEY ('.implode(', ', $this->primary).')' - ]), - $indexs, - $uniques, - $foreigns - ) - )."\n)"; - } - - public function run(array $values = null){ - return parent::execute($this->sql(), $values); - } +table = $table; + return $this; + } + + public function column(string $name, string $type, int $lenght = null, bool $not_null = false, string $more = null): Create{ + $this->columns[] = compact('name', 'type', 'lenght', 'not_null', 'more'); + return $this; + } + + public function primary(string $name): Create{ + $this->primary[] = $name; + return $this; + } + + public function unique(string $name, array $columns = null): Create{ + $this->unique[$name] = $columns ?? [$name]; + return $this; + } + + public function index(string $name, array $columns = null): Create{ + $this->index[$name] = $columns ?? [$name]; + return $this; + } + + public function foreign(string $name, string $table, string $column, string $on_delete = null, string $on_update = null): Create{ //TODO: complex foreign + $this->foreign[$name] = compact('name', 'table', 'column', 'on_delete', 'on_update'); + return $this; + } + + public function sql(){ + if(!isset($this->table)) + throw new \UnexpectedValueException('Any table set'); + + if(empty($this->columns)) + throw new \UnexpectedValueException('Any columns set'); + + $columns = []; + foreach($this->columns as $column){ + $columns[] = $column['name'].' '.$column['type'].($column['lenght'] ? '('.$column['lenght'].')' : '').($column['not_null'] ? ' NOT NULL' : '').(isset($column['more']) ? ' '.$column['more'] : ''); + } + + $uniques = []; + foreach ($this->unique as $name => $columns) { + $uniques[] = 'CONSTRAINT `UQ_'.ucfirst(strtolower($this->table)).'_'.ucfirst(strtolower($name)).'` UNIQUE ('.implode(', ', $columns).')'; + } + + $indexs = []; + foreach ($this->index as $name => $columns) { + $indexs[] = 'INDEX `ID_'.ucfirst(strtolower($this->table)).'_'.ucfirst(strtolower($name)).'` ('.implode(', ', $columns).')'; + } + + $foreigns = []; + foreach ($this->foreign as $name => $options) { + $foreigns[] = 'CONSTRAINT `FK_'.ucfirst(strtolower($this->table)).'_'.ucfirst(strtolower($name)).'` FOREIGN KEY (`'.$options['name'].'`) REFERENCES `'.$options['table'].'` (`'.$options['column'].'`)'. + (isset($options['on_delete']) ? 'ON DELETE '.$options['on_delete'] : ''). + (isset($options['on_update']) ? 'ON UPDATE '.$options['on_update'] : ''); + } + + return 'CREATE TABLE '.$this->table.'('."\n". + implode(",\n", + array_merge( + $columns, + (empty($this->primary) ? [] : [ + 'CONSTRAINT `PK_'.ucfirst(strtolower(strtok($this->table, ' '))).'` PRIMARY KEY ('.implode(', ', $this->primary).')' + ]), + $indexs, + $uniques, + $foreigns + ) + )."\n)"; + } + + public function run(array $values = null){ + return parent::execute($this->sql(), $values); + } } \ No newline at end of file diff --git a/src/Request/Data.php b/src/Request/Data.php index d496730..8002b64 100644 --- a/src/Request/Data.php +++ b/src/Request/Data.php @@ -1,21 +1,21 @@ -values = array_merge($this->values, $values); - }else{ - $this->values = $values; - } - return $this; - } - - public function execute(string $sql, array $values = null){ - $values = $values ? ($this->values ? array_merge($this->values, $values) : $values) : $this->values; - return parent::execute($sql, $values); - } +values = array_merge($this->values, $values); + else + $this->values = $values; + return $this; + } + + public function execute(string $sql, array $values = null){ + return parent::execute($sql, $values ? ($this->values ? array_merge($this->values, $values) : $values) : $this->values); + } } \ No newline at end of file diff --git a/src/Request/Delete.php b/src/Request/Delete.php index af7670d..7a749a6 100644 --- a/src/Request/Delete.php +++ b/src/Request/Delete.php @@ -1,36 +1,42 @@ -table = ($add && $this->table ? $this->table.', ' : '').$table; - return $this; - } - - public function where(string $where, bool $add = false): Delete{ - $this->where = $add && $this->where ? '('.$this->where.') AND ('.$where.')' : $where; - return $this; - } - - public function sql(){ - if(!isset($this->table)) - throw new DatabaseException('Any table set'); - - $sql = 'DELETE FROM '.$this->table. - ($this->where ? ("\n".'WHERE '.$this->where) : ''); - return $sql; - } - - public function run(array $values = null){ - return parent::execute($this->sql(), $values); - } +table = ($add && $this->table ? $this->table.', ' : '').$table; + return $this; + } + + /** + * @param string|array $where + * @param boolean $add + * @return Delete + */ + public function where($where, bool $add = false): Delete{ + $where = is_array($where) ? $where : [$where]; + $this->where = $add && $this->where ? array_merge($this->where, $where) : $where; + return $this; + } + + public function sql(){ + if(!isset($this->table)) + throw new \UnexpectedValueException('Any table set'); + + $sql = 'DELETE FROM '.$this->table. + ($this->where ? ("\n".'WHERE '.static::combineParams($this->where)) : ''); + return $sql; + } + + public function run(array $values = null){ + return parent::execute($this->sql(), $values); + } } \ No newline at end of file diff --git a/src/Request/Drop.php b/src/Request/Drop.php index 8afe34b..5409a08 100644 --- a/src/Request/Drop.php +++ b/src/Request/Drop.php @@ -1,26 +1,26 @@ -table = $table; - return $this; - } - - public function sql(){ - if(!isset($this->table)) - throw new DatabaseException('Any table set'); - - return 'DROP TABLE `'.$this->table.'`'; - } - - public function run(array $values = null){ - return parent::execute($this->sql(), $values); - } +table = $table; + return $this; + } + + public function sql(){ + if(!isset($this->table)) + throw new \UnexpectedValueException('Any table set'); + + return 'DROP TABLE `'.$this->table.'`'; + } + + public function run(array $values = null){ + return parent::execute($this->sql(), $values); + } } \ No newline at end of file diff --git a/src/Request/Insert.php b/src/Request/Insert.php index d062d7e..7c312c1 100644 --- a/src/Request/Insert.php +++ b/src/Request/Insert.php @@ -1,33 +1,33 @@ -fields = $add ? array_merge($this->fields, $fields) : $fields; - return $this; - } - - public function into(string $table): Insert{ - $this->table = $table; - return $this; - } - - public function sql(){ - if(!isset($this->table)) - throw new DatabaseException('Any table set'); - - return 'INSERT INTO `'.$this->table."`\n". - '('.implode(', ', $this->fields).")\n". - 'VALUES ('. str_repeat('?, ', count($this->fields)-1).(count($this->fields) > 0 ? '?' : '').')'; - } - - public function run(array $values = null){ - return parent::execute($this->sql(), $values); - } +fields = $add ? array_merge($this->fields, $fields) : $fields; + return $this; + } + + public function into(string $table): Insert{ + $this->table = $table; + return $this; + } + + public function sql(){ + if(!isset($this->table)) + throw new \UnexpectedValueException('Any table set'); + + return 'INSERT INTO '.$this->table."\n". + '('.implode(', ', $this->fields).")\n". + 'VALUES ('.static::paramList(count($this->fields)).')'; + } + + public function run(array $values = null){ + return parent::execute($this->sql(), $values); + } } \ No newline at end of file diff --git a/src/Request/Request.php b/src/Request/Request.php index f778b56..2d60009 100644 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -1,17 +1,42 @@ -db = $db; - } - - protected function execute(string $sql, array $values = null){ - return $this->db->execute($sql, $values, true); - } +db = $db; + } + + /** Run it */ + protected function execute(string $sql, array $values = null){ + return $this->db->execute($sql, $values, true); + } + + /*=== TOOLS ===*/ + public static function toParam(string $name, string $operator = '='): string{ + return $name.' '.$operator.' ?'; + } + + public static function toParams(array $names): string{ + return implode(', ', array_map(function($name){ return static::toParam($name); }, $names)); + } + + public static function paramList(int $count): string{ + return implode(',', array_fill(0, $count, '?')); + } + + public static function inParams(string $name, $params): string{ + return $name.' IN ('.static::paramList(is_int($params) ? $params : count($params)).')'; + } + + public static function combineParams(array $params, string $operator = ' AND '): string{ + return implode($operator, $params); + } } \ No newline at end of file diff --git a/src/Request/Select.php b/src/Request/Select.php index b522051..d1d2856 100644 --- a/src/Request/Select.php +++ b/src/Request/Select.php @@ -1,95 +1,102 @@ -fields = $add ? array_merge($this->fields, $fields) : $fields; - return $this; - } - - public function from(string $table, bool $add = false): Select{ - $this->table = ($add && $this->table ? $this->table.', ' : '').$table; - return $this; - } - - public function join(string $joins, string $type = 'INNER', bool $add = false): Select{ - if(!in_array($type, array('INNER', 'LEFT', 'RIGHT'))) - throw new DatabaseException('Unknown JOIN type'); - $this->joins = ($add && $this->joins ? $this->joins."\n" : '').$type.' JOIN '.$joins; - return $this; - } - - public function where(string $where, bool $add = false): Select{ - $this->where = $add && $this->where ? '('.$this->where.') AND ('.$where.')' : $where; - return $this; - } - - public function groupby(string $group): Select{ - $this->group = $group; - return $this; - } - - public function orderby(string $order): Select{ - $this->order = $order; - return $this; - } - - public function limit(string $limit): Select{ - $this->limit = $limit; - return $this; - } - - public function offset(string $offset): Select{ - $this->offset = $offset; - return $this; - } - - public function sql(){ - if(!isset($this->table)) - throw new DatabaseException('Any table set'); - - $fields = '*'; - if(isset($this->fields)){ - $numItems = count($this->fields); - $i = 0; - $fields = ''; - foreach($this->fields as $key => $value){ - $fields .= $value; - if(is_string($key)) - $fields .= ' '.$key; - - if(++$i !== $numItems) //Not last - $fields .= ', '; - } - } - - $sql = 'SELECT '.$fields. - "\n".'FROM '.$this->table. - ($this->joins ? ("\n".$this->joins) : ''). - ($this->where ? ("\n".'WHERE '.$this->where) : ''). - ($this->group ? ("\n".'GROUP BY '.$this->group) : ''). - ($this->order ? ("\n".'ORDER BY '.$this->order) : ''). - ($this->limit ? ("\n".'LIMIT '.$this->limit) : ''). - ($this->offset ? (($this->limit ? '' : "\n".'LIMIT 18446744073709551615').' OFFSET '.$this->offset) : ''); - return $sql; - } - - public function run(array $values = null){ - return parent::execute($this->sql(), $values); - } +fields = $add ? array_merge($this->fields, $fields) : $fields; + return $this; + } + + public function from(string $table, bool $add = false): Select{ + $this->table = ($add && $this->table ? $this->table.', ' : '').$table; + return $this; + } + + public function join(string $joins, string $type = 'INNER', bool $add = false): Select{ + if(!in_array($type, array('INNER', 'LEFT', 'RIGHT'))) + throw new \InvalidArgumentException('Unknown JOIN type'); + $this->joins = ($add && $this->joins ? $this->joins."\n" : '').$type.' JOIN '.$joins; + return $this; + } + + /** + * @param string|array $where + * @param boolean $add + * @return Select + */ + public function where($where, bool $add = false): Select{ + $where = is_array($where) ? $where : [$where]; + $this->where = $add && $this->where ? array_merge($this->where, $where) : $where; + return $this; + } + + public function groupby(string $group): Select{ + $this->group = $group; + return $this; + } + + public function orderby(string $order): Select{ + $this->order = $order; + return $this; + } + + public function limit(string $limit): Select{ + $this->limit = $limit; + return $this; + } + + public function offset(string $offset): Select{ + $this->offset = $offset; + return $this; + } + + public function sql(){ + if(!isset($this->table)) + throw new \UnexpectedValueException('Any table set'); + + $fields = '*'; + if(isset($this->fields)){ + $lines = []; + foreach($this->fields as $key => $value){ + $lines[] = $value.(is_string($key) ? ' '.$key : ''); + } + $fields = implode(', ', $lines); + } + + $sql = 'SELECT '.$fields. + "\n".'FROM '.$this->table. + ($this->joins ? ("\n".$this->joins) : ''). + ($this->where ? ("\n".'WHERE '.static::combineParams($this->where)) : ''). + ($this->group ? ("\n".'GROUP BY '.$this->group) : ''). + ($this->order ? ("\n".'ORDER BY '.$this->order) : ''). + ($this->limit ? ("\n".'LIMIT '.$this->limit) : ''). + ($this->offset ? (($this->limit ? '' : "\n".'LIMIT 18446744073709551615').' OFFSET '.$this->offset) : ''); + return $sql; + } + + public function run(array $values = null){ + return parent::execute($this->sql(), $values); + } } \ No newline at end of file diff --git a/src/Request/Update.php b/src/Request/Update.php index f200624..4b635cd 100644 --- a/src/Request/Update.php +++ b/src/Request/Update.php @@ -1,39 +1,51 @@ -fields = $add ? array_merge($this->fields, $fields) : $fields; - return $this; - } - - public function table(string $table): Update{ - $this->table = $table; - return $this; - } - - public function where(string $where, bool $add = false): Update{ - $this->where = $add && $this->where ? '('.$this->where.') AND ('.$where.')' : $where; - return $this; - } - - public function sql(){ - if(!isset($this->table)) - throw new DatabaseException('Any table set'); - - return 'UPDATE `'.$this->table."`\n". - 'SET '.implode(', ', array_map(function($field){ return $field.' = ?'; }, $this->fields))."\n". - (isset($this->where) ? ('WHERE '.$this->where) : ''); - } - - public function run(array $values = null){ - return parent::execute($this->sql(), $values); - } +fields = $add ? array_merge($this->fields, $fields) : $fields; + return $this; + } + + public function table(string $table): Update{ + $this->table = $table; + return $this; + } + + /** + * @param string|array $where + * @param boolean $add + * @return Update + */ + public function where($where, bool $add = false): Update{ + $where = is_array($where) ? $where : [$where]; + $this->where = $add && $this->where ? array_merge($this->where, $where) : $where; + return $this; + } + + public function sql(){ + if(!isset($this->table)) + throw new \UnexpectedValueException('Any table set'); + + return 'UPDATE '.$this->table."\n". + 'SET '.static::toParams($this->fields)."\n". + ($this->where ? 'WHERE '.static::combineParams($this->where) : ''); + } + + public function run(array $values = null){ + return parent::execute($this->sql(), $values); + } } \ No newline at end of file diff --git a/src/TypeHelper.php b/src/TypeHelper.php new file mode 100644 index 0000000..83f3c59 --- /dev/null +++ b/src/TypeHelper.php @@ -0,0 +1,21 @@ +format($format); + } + + public static function dateConvert($value): string{ + return static::dateTimeConverter('Y-m-d', $value); + } + + public static function timeConvert($value): string{ + return static::dateTimeConverter('H:i:s', $value); + } + + public static function datetimeConvert($value): string{ + return static::dateTimeConverter('Y-m-d H:i:s', $value); + } +} \ No newline at end of file