Foreign and comments

This commit is contained in:
sheychen 2018-06-20 10:50:58 +02:00
parent 4fb305a1c2
commit 3a36263208
1 changed files with 484 additions and 26 deletions

View File

@ -4,9 +4,9 @@ namespace Krutush\Database;
use Krutush\Database\DatabaseException; use Krutush\Database\DatabaseException;
//TODO extends /**
//TODO add model links * Static is a table, Object is an row
*/
class Model{ class Model{
/** @var string */ /** @var string */
public const DATABASE = null; 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']] 'owner' => ['type' => 'int', 'foreign' => ['model' => UserModel::class, 'field' => 'id', 'on_delete' => 'cascade', 'on_update' => 'set null']]
]*/ ]*/
/** @var array */
public const FOREIGNS = [];
/** @var string */ /** @var string */
public const ID = 'id'; public const ID = 'id';
/** @var array /**
* @example ['id' => ['value' => 1, 'modified' => false]] */ * @var array
* @example ['id' => ['value' => 1, 'modified' => false]]
*/
protected $fields = []; 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 FILTER = null;
public const INNER = null; //TODO: Manager OneToOne, OneToMany, ... public const INNER = null;
public const ORDER = null; public const ORDER = null;
/*=== MAGIC ===*/ /*=== 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){ public function __construct(array $data = [], bool $useColumns = false){
foreach (static::getFields() as $field => $options) { foreach (static::getFields() as $field => $options) {
$column = $useColumns ? static::getColumn($field) : $field; $column = $useColumns ? static::getColumn($field) : $field;
@ -43,13 +62,29 @@ class Model{
'modified' => false 'modified' => false
]; ];
} }
foreach(static::getForeigns() as $foreign => $options){
$this->fields[$foreign] = [
'modified' => false
];
}
} }
//MAYBE: Save on destroy //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){ public function __get(string $field){
if(array_key_exists($field, $this->fields)) if(strlen($field) > 0){
return $this->fields[$field]['value']; 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(); $trace = debug_backtrace();
trigger_error( trigger_error(
@ -58,8 +93,26 @@ class Model{
' à la ligne ' . $trace[0]['line'], ' à la ligne ' . $trace[0]['line'],
E_USER_NOTICE); E_USER_NOTICE);
return null; 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){ public function __set(string $field, $value){
if(array_key_exists($field, static::FIELDS)){ if(array_key_exists($field, static::FIELDS)){
$this->fields[$field] = [ $this->fields[$field] = [
@ -74,10 +127,101 @@ class Model{
' à la ligne ' . $trace[0]['line'], ' à la ligne ' . $trace[0]['line'],
E_USER_NOTICE); 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 ===*/
/**
* 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{ public static function fromRow(\PDOStatement $row, bool $exception = true): ?self{
if($row->rowCount() < 1){ if($row->rowCount() < 1){
if($exception) if($exception)
@ -89,6 +233,13 @@ class Model{
return new static($data, true); 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{ public static function fromRowAll(\PDOStatement $row, bool $exception = true): array{
if($row->rowCount() < 1){ if($row->rowCount() < 1){
if($exception) if($exception)
@ -103,7 +254,14 @@ class Model{
return $res; 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(); $res = array();
foreach($data as $element){ foreach($data as $element){
$res[] = new static($element, $useColumns); $res[] = new static($element, $useColumns);
@ -113,30 +271,112 @@ class Model{
/*=== CONST ===*/ /*=== 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'); throw new DatabaseException('FIELDS not set');
return static::FIELDS; return static::FIELDS;
} }
/**
* Get params for a specific field
*
* @param string $field
* @return array
*/
public static function getOptions(string $field): array{ public static function getOptions(string $field): array{
$fields = static::getFields(); $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); throw new DatabaseException('Can\'t find field : '.$field);
}
return $fields[$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{ public static function getColumn(string $field): string{
$options = static::getOptions($field); $options = static::getOptions($field);
return isset($options['column']) ? $options['column'] : $field; return isset($options['column']) ? $options['column'] : $field;
} }
/**
* Get table ID (for find and findOrFail)
*
* @return string
*/
public static function getID(): string{ public static function getID(): string{
return static::getColumn(static::ID); 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{ public static function getColumns(bool $sql = true): array{
$fields = static::getFields(); $fields = static::getFields();
$columns = []; $columns = [];
@ -147,6 +387,12 @@ class Model{
return $columns; 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{ public static function getPrimaryColumns(bool $sql = true): array{
$fields = static::getFields(); $fields = static::getFields();
$columns = []; $columns = [];
@ -159,11 +405,17 @@ class Model{
return $columns; 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{ public function getModifiedColumns(bool $sql = true): array{
$fields = static::getFields(); $fields = static::getFields();
$columns = []; $columns = [];
foreach ($fields as $field => $options) { foreach ($fields as $field => $options) {
if($this->fields[$field]['modified']){ if(isset($this->fields[$field]['modified']) && $this->fields[$field]['modified']){
$column = static::getColumn($field); $column = static::getColumn($field);
$columns[] = $sql ? '`'.static::TABLE.'`.`'.$column.'`' : $column; $columns[] = $sql ? '`'.static::TABLE.'`.`'.$column.'`' : $column;
} }
@ -171,30 +423,52 @@ class Model{
return $columns; return $columns;
} }
/**
* Get fields values (reverse of static::fromData)
*
* @return array
*/
public function getValues(){ public function getValues(){
$values = []; $values = [];
foreach ($this->fields as $field => $data) { foreach ($this->fields as $field => $data) {
$values[] = $data['value']; if(array_key_exists('value', $data))
$values[] = $data['value'];
} }
return $values; return $values;
} }
/**
* Same as getValues but only with 'modified' => true $this->fields
*
* @return array
*/
public function getModifiedValues(){ public function getModifiedValues(){
$values = []; $values = [];
foreach ($this->fields as $field => $data) { foreach ($this->fields as $field => $data) {
if($data['modified']) if(isset($data['modified']) && $data['modified'])
$values[] = $data['value']; $values[] = $data['value'];
} }
return $values; return $values;
} }
/**
* Remove all modified flags
*
* @return void
*/
public function unmodify(){ public function unmodify(){
foreach($this->fields as $field => $data){ 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(){ public function getPrimaryValues(){
$values = []; $values = [];
foreach ($this->fields as $field => $data) { foreach ($this->fields as $field => $data) {
@ -204,7 +478,14 @@ class Model{
return $values; 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); $options = static::getOptions($field);
if(is_null($data)){ if(is_null($data)){
if(isset($options['not_null']) && $options['not_null'] == true) if(isset($options['not_null']) && $options['not_null'] == true)
@ -246,10 +527,18 @@ class Model{
/*=== QUERIES ===*/ /*=== QUERIES ===*/
/**
* Create Request\Select from Model
*
* @return Request\Select
*/
public static function select(): Request\Select{ public static function select(): Request\Select{
$req = Connection::get(static::DATABASE) $req = Connection::get(static::DATABASE)
->select(static::getColumns()) ->select()->from(static::TABLE);
->from(static::TABLE);
if(!static::WILDCARD)
$req = $req->fields(static::getColumns());
if(static::INNER != null) if(static::INNER != null)
$req = $req->join(static::INNER); $req = $req->join(static::INNER);
@ -263,6 +552,11 @@ class Model{
return static::prepare($req); return static::prepare($req);
} }
/**
* Create Request\Insert (without data) from Model
*
* @return Request\Insert
*/
public static function insert(): Request\Insert{ public static function insert(): Request\Insert{
$req = Connection::get(static::DATABASE) $req = Connection::get(static::DATABASE)
->insert(static::getColumns()) ->insert(static::getColumns())
@ -271,15 +565,25 @@ class Model{
return $req; return $req;
} }
/**
* add values to insert and run it
*
* @param boolean $forceID must update id value
*/
public function runInsert(bool $forceID = true){ public function runInsert(bool $forceID = true){
$insert = static::insert(); $insert = static::insert();
$res = $insert->run($this->getValues()); $res = $insert->run($this->getValues());
if($forceID) if($forceID && static::ID != null)
$this->{static::ID} = Connection::get(static::DATABASE)->getLastInsertID(); $this->{static::ID} = Connection::get(static::DATABASE)->getLastInsertID();
return $res; return $res;
} }
/**
* Change value of modified fields in database
*
* @return Model
*/
public function runUpdate(){ public function runUpdate(){
$req = Connection::get(static::DATABASE) $req = Connection::get(static::DATABASE)
->update(static::getModifiedColumns()) ->update(static::getModifiedColumns())
@ -290,6 +594,11 @@ class Model{
return $this; return $this;
} }
/**
* Create Request\Create from Model
*
* @return Request\Create
*/
public static function create(): Request\Create{ public static function create(): Request\Create{
$req = Connection::get(static::DATABASE) $req = Connection::get(static::DATABASE)
->create(static::TABLE); ->create(static::TABLE);
@ -336,15 +645,33 @@ class Model{
return $req; 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) return Connection::get(static::DATABASE)
->drop(static::TABLE); ->drop(static::TABLE);
} }
/* Do advanced customuzation here */ /* 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(); $req = static::select();
if(isset($where)) if(isset($where))
@ -353,26 +680,68 @@ class Model{
return $req->run($values); 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{ public static function first(array $values = null, string $where = null): ?self{
return static::fromRow(static::runSelect($values, $where), false); 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)); 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{ public static function all(array $values = null, string $where = null): array{
return static::fromRowAll(static::runSelect($values, $where), false); 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{ public static function allOrFail(array $values = null, string $where = null): array{
return static::fromRowAll(static::runSelect($values, $where)); 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{ public static function exists(array $values = null, string $where = null): bool{
return static::first($values, $where) !== null; 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{ public static function count(array $values = null, string $where = null): int{
$req = static::select(); $req = static::select();
$req->fields(['COUNT(*) as count']); $req->fields(['COUNT(*) as count']);
@ -387,11 +756,100 @@ class Model{
return $data['count']; 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{ public static function find($id): ?self{
return static::first(array($id), (static::getID().' = ?')); 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{ public static function findOrFail($id): self{
return static::firstOrFail(array($id), (static::getID().' = ?')); 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;
}
} }