diff --git a/src/Model.php b/src/Model.php index 2a11136..01c00fa 100644 --- a/src/Model.php +++ b/src/Model.php @@ -4,9 +4,9 @@ namespace Krutush\Database; use Krutush\Database\DatabaseException; -//TODO extends -//TODO add model links - +/** + * Static is a table, Object is an row + */ class Model{ /** @var string */ public const DATABASE = null; @@ -21,19 +21,38 @@ class Model{ '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]] */ + /** + * @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; //TODO: Manager OneToOne, OneToMany, ... + 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; @@ -43,13 +62,29 @@ class Model{ '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(array_key_exists($field, $this->fields)) - return $this->fields[$field]['value']; + 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( @@ -58,8 +93,26 @@ class Model{ ' à 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] = [ @@ -74,10 +127,101 @@ class Model{ ' à 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) @@ -89,6 +233,13 @@ class Model{ 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) @@ -103,7 +254,14 @@ class Model{ return $res; } - public static function fromData($data, bool $useColumns = false): array{ + /** + * 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); @@ -113,30 +271,112 @@ class Model{ /*=== CONST ===*/ - public static function getFields(bool $exception = false): array{ - if(empty(static::$FIELDS) && $exception) + /** + * 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(!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 + * @return string + */ public static function getColumn(string $field): string{ $options = static::getOptions($field); return isset($options['column']) ? $options['column'] : $field; } + /** + * Get table ID (for find and findOrFail) + * + * @return string + */ public static function getID(): string{ return static::getColumn(static::ID); } + /** + * 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 = []; @@ -147,6 +387,12 @@ class Model{ 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 = []; @@ -159,11 +405,17 @@ class Model{ 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($this->fields[$field]['modified']){ + if(isset($this->fields[$field]['modified']) && $this->fields[$field]['modified']){ $column = static::getColumn($field); $columns[] = $sql ? '`'.static::TABLE.'`.`'.$column.'`' : $column; } @@ -171,30 +423,52 @@ class Model{ return $columns; } + /** + * Get fields values (reverse of static::fromData) + * + * @return array + */ public function getValues(){ $values = []; foreach ($this->fields as $field => $data) { - $values[] = $data['value']; + 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($data['modified']) + 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){ - $this->fields[$field]['modified'] = false; + 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) { @@ -204,7 +478,14 @@ class Model{ return $values; } - protected static function convertField($data, $field){ + /** + * 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) @@ -246,10 +527,18 @@ class Model{ /*=== QUERIES ===*/ + /** + * Create Request\Select from Model + * + * @return Request\Select + */ public static function select(): Request\Select{ $req = Connection::get(static::DATABASE) - ->select(static::getColumns()) - ->from(static::TABLE); + ->select()->from(static::TABLE); + + if(!static::WILDCARD) + $req = $req->fields(static::getColumns()); + if(static::INNER != null) $req = $req->join(static::INNER); @@ -263,6 +552,11 @@ class Model{ 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()) @@ -271,15 +565,25 @@ class Model{ 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) + 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()) @@ -290,6 +594,11 @@ class Model{ return $this; } + /** + * Create Request\Create from Model + * + * @return Request\Create + */ public static function create(): Request\Create{ $req = Connection::get(static::DATABASE) ->create(static::TABLE); @@ -336,15 +645,33 @@ class Model{ return $req; } - public static function drop(){ + /** + * 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 */ - protected static function prepare($req){ return $req; } + /** + * Modify default select request + * + * @param Request\Select $req + * @return Request\Select + */ + protected static function prepare(Request\Select $req): Request\Select{ return $req; } - public static function runSelect(array $values = null, string $where = null){ + /** + * 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)) @@ -353,26 +680,68 @@ class Model{ 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); } - public static function firstOrFail(array $values = null, string $where = null): ?self{ + /** + * 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']); @@ -387,11 +756,100 @@ class Model{ 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().' = ?')); } + + /** + * 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_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']) + 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; + } } \ No newline at end of file