This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
namespace EntityPHP;
abstract class Entity
{
protected $id=-1;
public static $foreign_sql=array();
public static $add_entities=array();
public static $bdd=null;
public static $utf8=false;
/**
* Create a connection to the database
* @static
* @access public
* @param string $host Host of database
* @param string $user Username for connection
* @param string $password Password for connection
* @param string $database Database you want to connect to
* @param bool $utf8 Do we have to use UTF-8 encoding ?
*/
final public static function connectToDB($host,$user,$password,$database,$utf8=true)
{
Entity::$bdd=new \PDO('mysql:host='.$host.';dbname='.$database,$user,$password);
if($utf8)
{
Entity::$bdd->exec('SET NAMES UTF8');
Entity::$utf8=true;
}
}
/**
* Get the classes inherited from Entity
* @static
* @access public
* @return array An array of classes as strings
*/
final public static function getEntities()
{
$entities=array();
foreach(get_declared_classes() as $class)
{
if(is_subclass_of($class,'EntityPHP\Entity'))
{
$entities[]=$class;
}
}
return $entities;
}
/**
* Get the SQL representation of a PHP value
* @static
* @access private
* @param mixed $value The value you want to "transform" into SQL
* @return string SQL representation of the given value
*/
final private static function php2sql($value)
{
switch(getType($value))
{
case 'integer': return 'INT('.$value.')';
case 'double': return 'FLOAT('.number_format($value,1,',','').')';
case 'boolean': return 'TINYINT(1)';
case 'string':
if(intval($value)==0)
{
return strtoupper($value);
}
else
{
return 'VARCHAR('.intval($value).')';
}
case 'array': return 'ENUM("'.implode('","',$value).'")';
}
}
/**
* Prepare the data of the given Entity object for an INSERT or UPDATE SQL request
* @static
* @access private
* @param string $entity The main class of the Entity object to prepare
* @param Entity $obj The object you want to prepare
* @param bool $update Do we have to prepare an UPDATE request?
* @return string SQL representation of the given value
*/
final private static function prepareDataForSQL($entity,$obj,$update=false)
{
$fields=get_class_vars($entity);
$fieldsSQL=array();
$valuesSQL=array();
$foreignSQL=array();
if(get_parent_class($obj)!='EntityPHP\Entity')
{
$fieldsSQL[]='subclass';
$valuesSQL[]='\''.get_class($obj).'\'';
}
//Iterate through each properties of the class
foreach($fields as $field => $value)
{
$prop=new \ReflectionProperty($entity,$field);
if(!$prop->isStatic() && $field!='id')
{
switch(getType($value))
{
case 'integer':
$fieldsSQL[]=$field;
$valuesSQL[]=intval($obj->$field);
break;
case 'double':
$fieldsSQL[]=$field;
$valuesSQL[]=floatval($obj->$field);
break;
case 'boolean':
$fieldsSQL[]=$field;
$valuesSQL[]=$obj->$field?1:0;
break;
case 'string':
if(class_exists($value) && $obj->$field==null)
{
$obj->load($field);
}
if($obj->$field==null || !class_exists($value) || !is_subclass_of($obj->$field,'EntityPHP\Entity'))
{
$fieldsSQL[]=$field;
switch(strtoupper($value))
{
case 'DATE':
$temp=is_numeric($obj->$field)?@date('Y-m-d',$obj->$field):(($obj->$field instanceof \DateTime)?$obj->$field->format('Y-m-d'):$obj->$field);
break;
case 'TIME':
$temp=is_numeric($obj->$field)?@date('H:i:s',$obj->$field):(($obj->$field instanceof \DateTime)?$obj->$field->format('h:i:s'):$obj->$field);
break;
case 'DATETIME':
$temp=is_numeric($obj->$field)?@date('Y-m-d H:i:s',$obj->$field):(($obj->$field instanceof \DateTime)?$obj->$field->format('Y-m-d H:i:s'):$obj->$field);
break;
case 'TIMESTAMP':
$temp=is_numeric($obj->$field)?@date('YmdHis',$obj->$field):(($obj->$field instanceof \DateTime)?$obj->$field->format('YmdHis'):$obj->$field);
break;
case 'YEAR':
$temp=(is_numeric($obj->$field)&&$obj->$field>9999)?@date('Y',$obj->$field):(($obj->$field instanceof \DateTime)?$obj->$field->format('Y'):$obj->$field);
break;
default:
$temp=htmlspecialchars($obj->$field,ENT_QUOTES,Entity::$utf8?'UTF-8':'ISO-8859-1');
break;
}
$valuesSQL[]='"'.$temp.'"';
}
else
{
$fieldsSQL[]='id_'.$field;
if(!($obj->$field instanceof $value))
{
$obj->load($field);
}
if($obj->$field instanceof $value)
{
if($obj->$field->getId()>-1)
{
$valuesSQL[]=$obj->$field->getId();
}
else
{
throw new \Exception('The field "'.$field.'" has to be an instance already saved in the DB.');
}
}
else
{
throw new \Exception('The field "'.$field.'" is not an instance of "'.$value.'".');
}
}
break;
case 'array':
if(class_exists($value[0])) //List of objects
{
$newId=0;
if($update)
{
$newId=$obj->getId();
}
else
{
$query=Entity::$bdd->query('SHOW TABLE STATUS LIKE "'.$entity.'"');
$donnees=$query->fetch(\PDO::FETCH_ASSOC);
$newId=$donnees['Auto_increment'];
}
$temp=array();
if(count($obj->$field)==0)
{
$obj->load($field);
}
$a = is_array($obj->$field)?$obj->$field:$obj->$field->getArray();
foreach($a as $key => $v)
{
if($v->existsInDB())
{
$temp[]='('.$newId.','.$v->getId().')';
}
else
{
$array=$obj->$field; //We have to use a temp variable... :(
unset($array[$key]);
}
}
if(!$obj->$field instanceof EntityArray)
{
$obj->$field=new EntityArray($value[0],$obj->$field);
}
if($update)
{
$foreignSQL[]='DELETE FROM '.$entity.'2'.$field.' WHERE id_'.$entity.'='.$newId;
}
$foreignSQL[]='INSERT INTO '.$entity.'2'.$field.' (id_'.$entity.',id_'.$field.') VALUES '.implode(',',$temp);
}
else //Enum
{
$fieldsSQL[]=$field;
$valuesSQL[]='"'.htmlspecialchars($obj->$field,ENT_QUOTES,Entity::$utf8?'UTF-8':'ISO-8859-1').'"';
}
break;
}
}
}
//Update the subclass
if(isset($obj->subclass))
{
$fieldsSQL[]='subclass';
$valuesSQL[]='"'.$obj->subclass.'"';
}
return array('fields'=>$fieldsSQL,'values'=>$valuesSQL,'foreign'=>$foreignSQL);
}
/**
* Get the SQL table name of the Entity which call this method
* @access public
* @static
* @return string Table name
*/
final public static function getTableName()
{
$tableName=get_called_class();
if($tableName!='EntityPHP\Entity')
{
while(get_parent_class($tableName)!='EntityPHP\Entity')
{
$tableName=get_parent_class($tableName);
}
return $tableName;
}
throw new \Exception('Entity::getTableName() : only subclasses of Entity can call this method.');
}
/**
* Create the database according to your Entities classes definition
* @static
* @access public
* @param bool $update Do we have to update tables?
*/
final public static function createDatabase($update=true)
{
//Only Entity class can call this method
if(get_called_class()=='EntityPHP\Entity')
{
$entities=Entity::getEntities();
$query=Entity::$bdd->query('SHOW TABLES');
$tables=array();
while($table=$query->fetch(\PDO::FETCH_NUM))
{
$tables[]=strtolower($table[0]);
}
foreach($entities as $entity)
{
$parent=get_parent_class($entity);
if($parent=='EntityPHP\Entity')
{
if(!in_array(strtolower($entity),$tables))
{
$entity::createTable();
}
else if($update)
{
$entity::updateTable();
}
}
else //Subclass of an Entity subclass
{
$classStats=new \ReflectionClass($entity);
if(!$classStats->isAbstract())
{
$parent=$parent::getTableName();
//Check if the main class has tu "subclasses" field
$query=Entity::$bdd->query('SELECT column_type FROM information_schema.columns WHERE table_name="'.$parent.'" AND column_name="subclass"');
if($query->rowCount()>0)
{
//If so, add our subclass into it
$enum=$query->fetch(\PDO::FETCH_NUM);
$enum=explode(',',substr(trim($enum[0]),5,-1));
if(!in_array('\''.$entity.'\'',$enum))
{
$enum[]='\''.$entity.'\'';
Entity::$bdd->exec('ALTER TABLE '.$parent.' CHANGE subclass subclass ENUM('.implode(',',$enum).') CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL');
}
}
else
{
Entity::$bdd->exec('ALTER TABLE '.$parent.' ADD subclass ENUM("'.$entity.'") CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL');
}
}
}
}
//If we have added foreign keys after an update of table
foreach(Entity::$add_entities as $entity)
{
if($entity::getById(1)==null)
{
$classStats=new \ReflectionClass($entity);
$abstract=false;
if($classStats->isAbstract())
{
//This class is abstract, we have to get one of its children
$abstract=true;
$entities=Entity::getEntities();
foreach($entities as $e)
{
if(is_subclass_of($e,$entity))
{
$classStats=new \ReflectionClass($e);
if(!$classStats->isAbstract())
{
$entity=$e;
$abstract=false;
}
}
}
}
if(!$abstract)
{
$entity::add(new $entity());
}
else
{
throw new \Exception('An error occurs during the update of one table : trying to add a foreign key referencing "'.$entity.'" class which seems to be abstract.');
}
}
}
foreach(Entity::$foreign_sql as $request)
{
Entity::$bdd->exec($request);
}
}
else
{
throw new \Exception('Only Entity class can call "createDatabase()".');
}
}
/**
* Create the SQL table of the Entity class which calls this method
* @access private
* @static
*/
final private static function createTable()
{
$entity=get_called_class();
if(get_parent_class($entity)=='EntityPHP\Entity')
{
$fields=get_class_vars($entity);
$sql='CREATE TABLE '.$entity.' (id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,';
$sql_foreign='';
foreach($fields as $field => $value)
{
$prop=new \ReflectionProperty($entity,$field);
if(!$prop->isStatic() && $field!='id')
{
//If the value is a string representing a class
if(is_string($value) && class_exists($value) && strtoupper($value)!='DATETIME')
{
//We can add a foreign key to this class
$sql.='id_'.$field.' INT(11) UNSIGNED NOT NULL, ';
$sql_foreign.='ADD CONSTRAINT fk_'.$entity.'_id_'.$field.' FOREIGN KEY (id_'.$field.') REFERENCES '.$value.'(id), ';
}
else
{
if(!is_array($value))
{
$sql.=$field.' '.self::php2sql($value).', ';
}
else
{
if(class_exists($value[0]))
{
Entity::$foreign_sql[]='CREATE TABLE '.$entity.'2'.$field.' (id_'.$entity.' INT(11) UNSIGNED NOT NULL, id_'.$field.' INT(11) UNSIGNED NOT NULL,CONSTRAINT FOREIGN KEY fk_'.$entity.'2'.$field.'_'.$entity.'$'.$entity.'2'.$value[0].' (id_'.$entity.') REFERENCES '.$entity.'(id) ON DELETE CASCADE, CONSTRAINT FOREIGN KEY fk_'.$entity.'2'.$field.'_'.$field.'$'.$entity.'2'.$value[0].' (id_'.$field.') REFERENCES '.$value[0].'(id) ON DELETE CASCADE) '.(Entity::$utf8?'DEFAULT CHARSET=utf8 ':'').'ENGINE=InnoDB';
}
else
{
$sql.=$field.' '.self::php2sql($value).', ';
}
}
}
}
}
Entity::$foreign_sql[]='ALTER TABLE '.$entity.' '.substr($sql_foreign,0,-2);
$sql=substr($sql,0,-2).') '.(Entity::$utf8?'DEFAULT CHARSET=utf8 ':'').'ENGINE=InnoDB';
Entity::$bdd->exec($sql);
}
else
{
throw new \Exception('Only direct subclasses of Entity can call "createTable()".');
}
}
/**
* Update the SQL table definition of the Entity class which calls this method
* @access private
* @static
*/
final private static function updateTable()
{
$entity=get_called_class();
if(get_parent_class($entity)=='EntityPHP\Entity')
{
$classFields=get_class_vars($entity);
$tableFields=array();
$tableFieldsClean=array();
$change=false;
$sql='';
$query=Entity::$bdd->query('SHOW COLUMNS FROM '.$entity);
$fields=$query->fetchAll(\PDO::FETCH_ASSOC);
foreach($fields as $field)
{
if($field['Field']!='id' && $field['Field']!='subclass')
{
$is_id=false;
//Test if the property is id_*
if(substr($field['Field'],0,3)=='id_')
{
$field['Field']=substr($field['Field'],3);
$is_id=true;
}
$tableFieldsClean[]=$field['Field'];
/* If the class doesn't have a property
having this name, we delete it */
if(!isset($classFields[$field['Field']]))
{
$sql.='DROP '.($is_id?'id_':'').$field['Field'].',';
$change=true;
/* Drop foreign key if exists */
$query=Entity::$bdd->query('SELECT NULL FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND CONSTRAINT_NAME = "fk_'.$entity.'_id_'.$field['Field'].'"');
if($query->rowCount()>0)
{
Entity::$bdd->exec('ALTER TABLE '.$entity.' DROP FOREIGN KEY fk_'.$entity.'_id_'.$field['Field']);
Entity::$bdd->exec('DROP INDEX fk_'.$entity.'_id_'.$field['Field'].' ON '.$entity);
}
}
else
{
$tableFields[$field['Field']]=trim(strtoupper($field['Type']));
}
}
}
/* $tableFields now contains all fields of
the table according to the SQL database. */
$sql_foreign='';
foreach($classFields as $field => $value)
{
if($field!='id')
{
$prop=new \ReflectionProperty($entity,$field);
if(!$prop->isStatic())
{
$type=self::php2sql($value);
//If the class property is not in the SQL definition, we'll add it
if(!isset($tableFields[$field]))
{
//If the value is a string not containing 'TEXT'
if(is_string($value) && intval($value)==0 && $value!='TEXT')
{
if(class_exists($value))
{
$sql.='ADD id_'.$field.' INT(11) UNSIGNED NOT NULL DEFAULT 1,';
$sql_foreign.='ADD CONSTRAINT fk_'.$entity.'_id_'.$field.' FOREIGN KEY (id_'.$field.') REFERENCES '.$value.'(id), ';
$change=true;
if($entity::count()>0)
{
Entity::$add_entities[]=$value;
}
}
}
else
{
if(is_array($value))
{
if(class_exists($value[0]))
{
Entity::$foreign_sql[]='CREATE TABLE '.$entity.'2'.$field.' (id_'.$entity.' INT(11) UNSIGNED NOT NULL, id_'.$field.' INT(11) UNSIGNED NOT NULL,CONSTRAINT FOREIGN KEY fk_'.$entity.'2'.$field.'_'.$entity.'$'.$entity.'2'.$value[0].' (id_'.$entity.') REFERENCES '.$entity.'(id) ON DELETE CASCADE, CONSTRAINT FOREIGN KEY fk_'.$entity.'2'.$field.'_'.$field.'$'.$entity.'2'.$value[0].' (id_'.$field.') REFERENCES '.$value[0].'(id) ON DELETE CASCADE) '.(Entity::$utf8?'DEFAULT CHARSET=utf8 ':'').'ENGINE=InnoDB';
}
else
{
$sql.='ADD '.$field.' '.$type.',';
}
}
else
{
$sql.='ADD '.$field.' '.$type.',';
}
$change=true;
}
}
else if(str_replace('"','\'',strtoupper($type))!=str_replace('"','\'',strtoupper($tableFields[$field]))) //Otherwise we check for changes
{
//If the value is a string not containing 'TEXT'
if(is_string($value) && intval($value)===0 && $value!='TEXT' && !in_array($field,$tableFieldsClean))
{
if(class_exists($value))
{
$sql.='MODIFY id_'.$field.' INT(11) UNSIGNED NOT NULL DEFAULT 1,';
$sql_foreign.='ADD CONSTRAINT fk_'.$entity.'_id_'.$field.' FOREIGN KEY (id_'.$field.') REFERENCES '.$value.'(id), ';
$change=true;
if($entity::count()>0)
{
Entity::$add_entities[]=$value;
}
}
}
else if(is_string($value))
{
if(!class_exists($value))
{
$sql.='MODIFY '.$field.' '.$type.',';
$change=true;
}
}
else if(is_array($value))
{
if(class_exists($value[0]))
{
Entity::$foreign_sql[]='CREATE TABLE '.$entity.'2'.$field.' (id_'.$entity.' INT(11) UNSIGNED NOT NULL, id_'.$field.' INT(11) UNSIGNED NOT NULL,CONSTRAINT FOREIGN KEY fk_'.$entity.'2'.$field.'_'.$entity.'$'.$entity.'2'.$value[0].' (id_'.$entity.') REFERENCES '.$entity.'(id) ON DELETE CASCADE, CONSTRAINT FOREIGN KEY fk_'.$entity.'2'.$field.'_'.$field.'$'.$entity.'2'.$value[0].' (id_'.$field.') REFERENCES '.$value[0].'(id) ON DELETE CASCADE) '.(Entity::$utf8?'DEFAULT CHARSET=utf8 ':'').'ENGINE=InnoDB';
}
else
{
$sql.='ADD '.$field.' '.$type.',';
}
$change=true;
}
}
}
}
}
Entity::$foreign_sql[]='ALTER TABLE '.$entity.' '.substr($sql_foreign,0,-2);
if($change)
{
// echo 'ALTER TABLE '.$entity.' '.substr($sql,0,-1);
Entity::$bdd->exec('ALTER TABLE '.$entity.' '.substr($sql,0,-1));
}
}
else
{
throw new \Exception('Only direct subclasses of Entity can call "updateTable()".');
}
}
/**
* Delete the SQL table of the Entity class which calls this method
* @access public
* @static
*/
final public static function deleteTable()
{
$entity=get_called_class();
if(get_parent_class($entity)=='EntityPHP\Entity')
{
if(!Entity::$bdd->exec('DROP TABLE '.$entity))
{
$query=Entity::$bdd->query('SELECT DISTINCT table_name FROM information_schema.statistics WHERE index_name LIKE "$'.$entity.'2%" OR index_name LIKE "%2'.$entity.'" OR index_name LIKE "fk_'.$entity.'_%"');
if($query->rowCount()>0)
{
while($donnees=$query->fetch(\PDO::FETCH_NUM))
{
Entity::$bdd->exec('DROP TABLE '.$donnees[0]);
}
Entity::$bdd->exec('DROP TABLE '.$entity);
}
}
}
else
{
throw new \Exception('Only direct subclasses of Entity can call "deleteTable()".');
}
}
/**
* Return the id of the instance
* @access public
* @return int Id of the instance
*/
final public function getId()
{
return $this->id;
}
/**
* Check if the instance is saved in the database
* @access public
* @return bool TRUE if the instance exists, FALSE otherwise
*/
final public function existsInDB()
{
$query=Entity::$bdd->query('SELECT NULL FROM '.self::getTableName().' WHERE id='.$this->id);
return $query->rowCount()>0;
}
/**
* Check if the given id is used in the database
* @static
* @access public
* @param int $id The id to check
* @return bool TRUE if the id is used, FALSE otherwise
*/
final public static function idExistsInDB($id)
{
$query=Entity::$bdd->query('SELECT NULL FROM '.self::getTableName().' WHERE id='.intval($id));
return $query->rowCount()>0;
}
/**
* Indicates if two entities are the same one
* @access public
* @param Entity $other The Entity to check
* @return Bool True if the entities are the same one. False otherwise.
*/
final public function equals(Entity $other)
{
return get_class($this)==get_class($other) && $this->id==$other->getId();
}
/**
* Get an Entity from the called class by its id
* @static
* @access public
* @param int $id The id to check
* @return Entity|NULL The Entity with the given id if found. NULL otherwise.
*/
final public static function getById($id)
{
if(get_called_class()!='EntityPHP\Entity')
{
return self::createRequest()
->where('id=?',array(intval($id)))
->getOnly(1)
->exec();
}
else
{
throw new \Exception('Entity::getById(Int $id) -> Only a subclass of Entity can call this method.');
}
}
/**
* Get all entities from the called class
* @static
* @access public
* @return EntityArray An EntityArray containing the found instances
*/
final public static function getAll()
{
$entity=get_called_class();
if($entity!='EntityPHP\Entity')
{
return self::createRequest()->exec();
}
else
{
throw new \Exception('Entity::getAll() -> Only a subclass of Entity can call this method.');
}
}
/**
* Create an EntityRequest for the called class
* @static
* @access public
* @return EntityRequest The EntityRequest corresponding to this Entity class
*/
final public static function createRequest()
{
return new EntityRequest(self::getTableName());
}
/**
* Simply a direct call to get_class_vars, to permit the public access
* @static
* @access public
* @return Array() The result of get_class_vars() for this class
*/
final public static function getVars()
{
$entity=get_called_class();
$vars=get_class_vars($entity);
$return=array();
foreach($vars as $var=>$value)
{
$prop=new \ReflectionProperty($entity,$var);
if(!$prop->isStatic())
{
$return[$var]=$value;
}
}
return $return;
}
/**
* Load given properties of the Entity
* @access public
* @param string $prop The property to load
* @return Entity The called Entity
*/
final public function load($prop)
{
$prop=trim($prop);
$entity=get_class($this);
$fields=get_class_vars($entity);
if(isset($fields[$prop]))
{
if((is_string($fields[$prop]) && class_exists($fields[$prop]))||(is_array($fields[$prop]) && class_exists($fields[$prop][0])))
{
//One to many
if(is_string($fields[$prop]))
{
if(is_subclass_of($fields[$prop],'EntityPHP\Entity'))
{
$this->$prop=$fields[$prop]::getById($this->{'id_'.$prop});
unset($this->{'id_'.$prop});
}
else
{
throw new \Exception('Entity::load(String $prop) : Property "'.$prop.'" from "'.$entity.'" is not referencing to an Entity object.');
}
}
else //Many to Many
{
$thisTable=$entity::getTableName();
$this->$prop=$thisTable::createRequest()
->select($prop)
->where('id=?',array($this->id))
->exec();
}
return $this;
}
throw new \Exception('Entity::load(String $prop) : Property "'.$prop.'" from "'.$entity.'" is not referencing to object(s).');
}
throw new \Exception('Entity::load(String $prop) : "'.$entity.'" has no properties named "'.$prop.'".');
}
/**
* Inidicates if the called class is a subclass of a subclass of Entity
* @static
* @access public
* @return bool True if it's a subclass. False otherwise.
*/
public static function isAnInheritedClass()
{
return get_parent_class(get_called_class())!='EntityPHP\Entity';
}
/**
* Count the number of entities of the called class
* @static
* @access public
* @return int Total of entities
*/
final public static function count()
{
$entity=get_called_class();
if($entity!='EntityPHP\Entity')
{
$query=Entity::$bdd->query('SELECT NULL FROM '.$entity::getTableName().' '.(self::isAnInheritedClass()?' WHERE subclass="'.$entity.'"':''));
return $query->rowCount();
}
else
{
throw new \Exception('Entity::count() -> Only a subclass of Entity can call this method.');
}
}
/**
* Save a new Entity instance in database
* @static
* @access public
* @param Entity $obj Instance to persist
* @return Entity The stored instance
*/
public static function add(Entity $obj)
{
$entity=get_called_class();
if($entity!='EntityPHP\Entity')
{
if($obj instanceof $entity)
{
$parent=$entity::getTableName();
$sql=Entity::prepareDataForSQL($parent,$obj);
Entity::$bdd->exec('INSERT INTO '.$parent.' ('.implode(',',$sql['fields']).') VALUES ('.implode(',',$sql['values']).')');
$obj->id=Entity::$bdd->lastInsertId();
foreach($sql['foreign'] as $request)
{
Entity::$bdd->exec($request);
}
return $obj;
}
else
{
throw new \Exception('Entity::add(Entity $obj) -> given $obj is not an instance of this class.');
}
}
else
{
throw new \Exception('Entity::add(Entity $obj) -> Only a subclass of Entity can call this method.');
}
}
/**
* Update an Entity instance in database
* @static
* @access public
* @param Entity $obj Instance to persist
* @return Entity The updated instance
*/
final public static function update(Entity $obj)
{
$entity=get_called_class();
if($entity!='EntityPHP\Entity')
{
if($obj instanceof $entity)
{
$parent=$entity::getTableName();
$query=Entity::$bdd->query('SELECT id FROM '.$parent.' WHERE id='.intval($obj->id));
if($query->rowCount()>0)
{
$sql=Entity::prepareDataForSQL($parent,$obj,true);
$set=array();
for($i=0,$l=count($sql['fields']); $i<$l; $i++)
{
$set[]=$sql['fields'][$i].'='.$sql['values'][$i];
}
foreach($sql['foreign'] as $request)
{
Entity::$bdd->exec($request);
}
Entity::$bdd->exec('UPDATE '.$parent.' SET '.implode(',',$set).' WHERE id='.intval($obj->id));
return $parent::getById($obj->id);
}
else
{
throw new \Exception('Entity::update(Entity $obj) -> given $obj seems to not exist in the DB.');
}
}
else
{
throw new \Exception('Entity::update(Entity $obj) -> given $obj is not an instance of this class.');
}
}
else
{
throw new \Exception('Entity::update(Entity $obj) -> Only a subclass of Entity can call this method.');
}
}
/**
* Delete an Entity instance in database
* @static
* @access public
* @param Entity $obj Instance to delete
*/
final public static function delete(Entity $obj)
{
$entity=get_called_class();
if($entity!='EntityPHP\Entity')
{
if($obj instanceof $entity)
{
$parent=$entity::getTableName();
$query=Entity::$bdd->query('SELECT id FROM '.$parent.' WHERE id='.intval($obj->id));
if($query->rowCount()>0)
{
Entity::$bdd->exec('DELETE FROM '.$parent.' WHERE id='.intval($obj->id));
}
else
{
throw new \Exception('Entity::delete(Entity $obj) -> given $obj seems to not exist in the DB.');
}
}
else
{
throw new \Exception('Entity::delete(Entity $obj) -> given $obj is not an instance of this class.');
}
}
else
{
throw new \Exception('Entity::delete(Entity $obj) -> Only a subclass of Entity can call this method.');
}
}
/**
* Allow to echo an Entity
* @access public
* @return string The string representation of the Entity
*/
public function __toString()
{
return '
'.print_r($this,true).'
';
}
/**
* Return a JSON representation of the Entity
* @access public
* @return string The JSON representation of the Entity
*/
public function toJSON()
{
$json=array();
foreach($this as $key=>$value)
{
$json[$key]=$value;
}
return json_encode($json,JSON_FORCE_OBJECT);
}
}
//We don't extend ArrayIterator, because I don't like it =D
final class EntityArray implements \SeekableIterator, \ArrayAccess, \Countable
{
private $i=0;
private $array=array();
private $entity='';
/* SeekableIterator */
public function current()
{
return $this->offsetGet($this->i);
}
public function key()
{
return $this->i;
}
public function next()
{
$this->i++;
}
public function valid()
{
return isset($this->array[$this->i]);
}
public function rewind()
{
$this->i=0;
}
public function seek($i)
{
$old=$this->i;
$this->i=$i;
if(!$this->valid())
{
$this->i=$old;
}
}
/* ArrayAccess */
public function offsetExists($i)
{
return isset ($this->array[$i]);
}
public function offsetGet($i)
{
return $this->offsetExists($i)?$this->array[$i]:null;
}
public function offsetSet($i,$value)
{
if($value instanceof $this->entity)
{
if($value->existsInDB())
{
$this->array[$i]=$value;
}
else
{
throw new \Exception('EntityArray : the given value must be saved in the DB.');
}
}
else
{
throw new \Exception('EntityArray : the given value must be an instance of "'.$this->entity.'".');
}
}
public function offsetUnset($i)
{
unset($this->array[$i]);
}
/* Countable */
public function count(Entity $entity=null)
{
if(is_null($entity))
{
return count($this->array);
}
$i=0;
foreach($this->array as $obj)
{
if($obj->equals($entity))
{
$i++;
}
}
return $i;
}
/* EntityArray */
public function __construct($entity,$array=array())
{
if(is_string($entity))
{
$this->array=$array;
$this->entity=$entity::getTableName();
}
else
{
throw new \Exception('First parameter of EntityArray must be an Entity classname.');
}
}
/**
* Return the array stored in the EntityArray
* @access public
* @return Array The array of the EntityArray
*/
public function getArray()
{
return $this->array;
}
/**
* Return the first Entity contained in the array
* @access public
* @return Entity The first Entity
*/
public function getFirst()
{
return $this->offsetGet(0);
}
/**
* Return the last Entity contained in the array
* @access public
* @return Entity The last Entity
*/
public function getLast()
{
return $this->offsetGet($this->count()-1);
}
/**
* Get randomly one or several entities from the array
* @access public
* @param int $total Number of entities to get
* @return Entity|EntityArray The Entity randomly gotten if $total is 1, an EntityArray otherwise
*/
public function getRandom($total=1)
{
$total=max(1,min(count($this->array),intval($total)));
if($total==1)
{
return $this->offsetGet(rand(0,$this->count()-1));
}
else if($total==$this->count())
{
$return=new EntityArray($this->entity,$this->array);
return $return->shuffle();
}
else
{
$temp=$this->array;
$return=array();
for($i=0;$i<$total;$i++)
{
$obj=array_splice($temp, rand(0,count($temp)-1),1);
$return[]=$obj[0];
}
return new EntityArray($this->entity,$return);
}
}
/**
* Add an entity at the end of the array
* @access public
* @param Entity $obj The entity to add
* @return EntityArray The calling EntityArray
*/
public function push(Entity $obj)
{
if($obj instanceof $this->entity)
{
$this->array[]=$obj;
return $this;
}
throw new \Exception('EntityArray::push(Entity $obj) : the given value must be an instance of "'.$this->entity.'".');
}
/**
* Remove an Entity from the array at the given index
* @access public
* @param Int $i The index to remove
* @return EntityArray The calling EntityArray
*/
public function removeIndex($i=0)
{
if(isset($this->array[$i]))
{
unset($this->array[$i]);
return $this;
}
throw new \Exception('EntityArray::removeIndex(Int $i) : no instances found at index '.$i.'.');
}
/**
* Remove the given Entity from the array
* @access public
* @param Entity $entity The entity to remove (default: null)
* @param Bool $justFirst If true, remove only the first found entity. Remove all found entities otherwise. (default: true)
* @return EntityArray The calling EntityArray
*/
public function remove(Entity $entity=null, $justFirst=true)
{
foreach($this->array as $key => $obj)
{
if($obj->equals($entity))
{
unset($this->array[$key]);
if($justFirst)
{
return $this;
}
}
}
return $this;
}
/**
* Reverse the order of the array
* @access public
* @return EntityArray The calling EntityArray
*/
public function reverse()
{
$this->array=array_reverse($this->array);
return $this;
}
/**
* Suffle the array
* @access public
* @return EntityArray The calling EntityArray
*/
public function shuffle()
{
shuffle($this->array);
return $this;
}
/**
* Indicates if the array contains the given Entity
* @access public
* @param Entity $other The Entity to check
* @return Bool True if the entity is in the array. False otherwise.
*/
public function hasEntity(Entity $other)
{
foreach($this->array as $obj)
{
if($other->equals($obj))
{
return true;
}
}
return false;
}
/**
* Removes duplicate values from the array
* @access public
* @param Bool $set If True, the array will be changed. If False, the method will simply return a new EntityArray without duplicate values. (default: True)
* @return EntityArray The calling filtered EntityArray if $set is True. A filtered copy of this EntityArray otherwise.
*/
public function unique($set=true)
{
$tempEntity=new EntityArray($this->entity);
foreach($this->array as $obj)
{
if(!$tempEntity->hasEntity($obj))
{
$tempEntity->array[]=$obj;
}
}
if($set)
{
$this->array=$tempEntity->getArray();
return $this;
}
else
{
return $tempEntity;
}
}
}
final class EntityRequest
{
private $entity='';
private $select='*';
private $where='1=1';
private $orderBy=1;
private $totalPropertiesToSelect=0;
private $limit='';
private $join='';
private $joinedTables=array();
private $canFetchClass=true;
private $fetchClassName='';
private $totalToGet=999;
public function __construct($entity='')
{
if(is_string($entity) && !empty($entity))
{
$this->entity=$entity::getTableName();
$this->fetchClassName=$this->entity;
}
else
{
throw new \Exception('Parameter of EntityRequest must be an Entity classname.');
}
}
/**
* Generate a SQL join request according to the given values
* @access private
* @param String $table The table name to join
* @param String $property The property name used to link the two tables
* @param String $originTable The table name used to base the join (default: $this->entity)
* @return EntityRequest The calling EntityRequest.
*/
private function join($table,$property,$originTable='')
{
if(!in_array($table,$this->joinedTables))
{
$originTable=empty($originTable)?$this->entity:$originTable;
$this->join.=' JOIN '.$table.' ON '.$table.'.id='.$originTable.'.id_'.$property;
$this->joinedTables[]=$table;
}
return $this;
}
/**
* Generate a SQL join request for Many to Many relations according to the given values
* @access private
* @param String $table The table name to join
* @param String $property The property name used to link the two tables via the linked table
* @param String $originTable The table name used to base the join (default: $this->entity)
* @return EntityRequest The calling EntityRequest.
*/
private function joinMany2Many($table,$property,$originTable='')
{
$originTable=empty($originTable)?$this->entity:$originTable;
$newTable=$originTable.'2'.$table;
if(!in_array($newTable,$this->joinedTables))
{
$this->join.=' JOIN '.$newTable.' ON '.$newTable.'.id_'.$originTable.'='.$originTable.'.id';
$this->joinedTables[]=$newTable;
}
if(!in_array($table,$this->joinedTables))
{
$this->join.=' JOIN '.$table.' ON '.$table.'.id='.$newTable.'.id_'.$property;
$this->joinedTables[]=$table;
}
return $this;
}
/**
* Analyze a property name in order to detect subproperties and generate the SQL request
* @access private
* @param String $entity Class name containing the property $prop
* @param String $prop The property to analyze
* @param String $type Type of request to generate with this analyse (select, where, order). (default: select)
* @param String $parentProp The name of the parent property if $prop is a subproperty (default: '')
* @return Void|String Void if $type equals "select", a string wich will replace a part of a WHERE request if $type equals "where"
*/
private function analyzeProperty($entity,$prop,$type='select',$parentProp='')
{
$vars=$entity::getVars();
$fields=explode('.',trim($prop));
$prop=array_shift($fields);
$errorMethod=$type=='where'?'where(String $props, Array $values)':($type=='orderBy'?'orderBy(String $props)':'select(String $props)');
//Check if property exists
if(isset($vars[$prop]))
{
$totalSplit=count($fields);
$prop_value=$vars[$prop];
$addSimpleProperty=function() use ($entity,$prop,$type,$parentProp)
{
switch($type)
{
case 'select':
return ','.$entity.'.'.$prop.(!empty($parentProp)?' AS "'.str_replace('.','_',$parentProp).'_'.$prop.'"':'');
break;
case 'where':
return $entity.'.'.$prop;
case 'orderBy':
return ','.$entity.'.'.$prop;
}
};
if(is_string($prop_value) || is_array($prop_value)) //Could be an Entity, a set of entities or a simple property
{
$className=is_array($prop_value)?$prop_value[0]:$prop_value;
if(class_exists($className)) //A class name!
{
if(!is_subclass_of($className,'EntityPHP\Entity'))
{
throw new \Exception('EntityRequest::'.$errorMethod.' : "'.$entity.'.'.$prop.'" is not a subclass of Entity.');
}
$otherTableName=$className::getTableName();
if(is_array($prop_value))
{
$this->joinMany2Many($otherTableName,$prop,$entity);
}
else
{
$this->join($otherTableName,$prop,$entity);
}
if($totalSplit>0) //We want to select a property of the contained Entity
{
return $this->analyzeProperty($className,implode('.',$fields),$type,(!empty($parentProp)?$parentProp.'.':'').$prop);
}
else //We want to select ALL properties of the contained Entity
{
if($type=='select')
{
$this->generateSelectAll($otherTableName,$prop);
}
else
{
throw new \Exception('EntityRequest::'.$errorMethod.' : "'.$entity.'.'.$prop.'" can\'t be used for this method.');
}
}
}
else //Simple property
{
return $addSimpleProperty();
}
}
else //Simple property
{
return $addSimpleProperty();
}
}
else
{
throw new \Exception('EntityRequest::'.$errorMethod.' : "'.$entity.'" has no properties named "'.$prop.'".');
}
}
/**
* Generate a SQL select request according to the given value
* @access public
* @param String $props The properties to select form the table, separated by comas.
* @return EntityRequest The calling EntityRequest.
*/
public function select($props)
{
$this->totalPropertiesToSelect=0;
if($props=='*')
{
$this->select='*';
$this->canFetchClass=true;
$this->fetchClassName=$this->entity;
}
else
{
$this->select='';
$this->canFetchClass=false;
$props=explode(',',$props);
foreach($props as $prop)
{
$this->totalPropertiesToSelect++;
$this->select.=$this->analyzeProperty($this->entity,$prop);
}
$this->select=substr($this->select,1);
}
return $this;
}
/**
* Generate a SQL where request according to the given values
* @access public
* @param String $props The query used to filter
* @param Array $values Values of variables noted as ? in $props (default: array())
* @return EntityRequest The calling EntityRequest.
*/
public function where($props,Array $values=array())
{
if($props=='1=1')
{
$this->where='1=1';
}
else
{
if(substr_count($props,'?')==count($values))
{
$entity=$this->entity;
$vars=$entity::getVars();
$keywords=array('AND','OR','BETWEEN','IN','\(','\)','!=','<=','>=','<','>','=','\*','\+','-','/');
$props=str_replace(' ','',trim($props));
$props=preg_replace('#('.implode('|',$keywords).')#sU',' $1 ',$props);
$indexData=0;
$props=explode(' ',$props);
foreach($props as $key => $elem)
{
if(!in_array($elem,$keywords) && $elem!='(' && $elem !=')' && $elem !='*' && $elem !='+' && strlen($elem)>0)
{
if($elem=='?')
{
$value=$values[$indexData++];
if(!is_numeric($value))
{
$value='"'.htmlspecialchars($value,ENT_QUOTES,Entity::$utf8?'UTF-8':'ISO-8859-1').'"';
}
$props[$key]=$value;
}
else
{
$props[$key]=$this->analyzeProperty($this->entity,$elem,'where');
}
}
}
$this->where=implode(' ',$props);
}
else
{
throw new \Exception('EntityRequest::where(String $props, Array $values) : $props doesn\'t contain as much "?" than the number of data in $values.');
}
}
return $this;
}
/**
* Generate a SQL order request according to the given values
* @access public
* @param String $props The properties used to order the results, separated by comas.
* @return EntityRequest The calling EntityRequest.
*/
public function orderBy($props)
{
$this->orderBy='';
$entity=$this->entity;
$class=new \ReflectionClass($entity);
//We can order according to several properties
$props=explode(',',$props);
foreach($props as $prop)
{
$desc=false;
if(substr_count($prop,' DESC')>0 || substr_count($prop,' desc')>0)
{
$desc=true;
$prop=str_replace(' DESC','',str_replace(' desc','',$prop));
}
$this->orderBy.=$this->analyzeProperty($this->entity,$prop,'orderBy').($desc?' DESC':'');
}
$this->orderBy=substr($this->orderBy,1);
return $this;
}
/**
* Generate a SQL limit request according to the given values
* @access public
* @param Int $total The maximum number of results to return
* @param Int $fromRecord The offset of the first result to return (default: 0)
* @return EntityRequest The calling EntityRequest.
*/
public function getOnly($total,$fromRecord=0)
{
$this->totalToGet=$total;
$this->limit=' LIMIT '.$fromRecord.','.$total;
return $this;
}
/**
* Set a SELECT * SQL request for the given entity
* @access private
* @param String $entity Entity classname we want to make a SELECT * request
* @param String $prop The property name of the Entity in case of JOIN request (default: '')
* @return EntityRequest The calling EntityRequest
*/
private function generateSelectAll($entity,$prop='')
{
$vars=$entity::getVars();
$setAlias=!$this->canFetchClass&&!empty($prop)&&$this->totalPropertiesToSelect>1;
if($this->totalPropertiesToSelect==1)
{
$this->canFetchClass=true;
$this->fetchClassName=$entity;
}
foreach($vars as $key => $var)
{
if(is_string($var))
{
$this->select.=','.$entity.'.';
if(class_exists($var) && is_subclass_of($var,'EntityPHP\Entity'))
{
$this->select.='id_'.$key;
}
else
{
$this->select.=$key;
}
}
else if(is_array($var))
{
if(!class_exists($var[0]) || !is_subclass_of($var[0],'EntityPHP\Entity'))
{
$this->select.=','.$entity.'.'.$key;
}
}
else
{
$this->select.=','.$entity.'.'.$key;
}
$this->select.=($setAlias?' AS "'.$prop.'_'.$key.'"':'');
}
return $this;
}
/**
* Generate and return the complete SQL request of this EntityRequest
* @access public
* @return String The SQL request
*/
public function getSQLRequest()
{
if($this->select=='*')
{
$this->select='';
$this->generateSelectAll($this->entity);
$this->select=substr($this->select,1);
}
if($this->orderBy==1)
{
$this->orderBy=$this->entity.'.id';
}
return 'SELECT '.$this->select.' FROM '.$this->entity.$this->join.' WHERE '.$this->where.' ORDER BY '.$this->orderBy.$this->limit.';';
}
/**
* Execute the SQL query stored in this EntityRequest
* @access public
* @return EntityArray An EntityArray containing the entities gotten form the request
*/
public function exec()
{
$query=Entity::$bdd->query($this->getSQLRequest());
$return=array();
if($query)
{
if($query->rowCount()>0)
{
if($this->canFetchClass)
{
$query->setFetchMode(\PDO::FETCH_CLASS|\PDO::FETCH_PROPS_LATE,$this->fetchClassName);
}
else
{
$query->setFetchMode(\PDO::FETCH_OBJ);
}
while($obj=$query->fetch())
{
$return[]=$obj;
}
}
if($this->totalToGet>1)
{
return $this->canFetchClass?new EntityArray($this->fetchClassName,$return):$return;
}
else
{
return isset($return[0])?$return[0]:null;
}
}
$error=Entity::$bdd->errorInfo();
throw new \Exception("EntityRequest::exec() : An error occurs while running the generated SQL request.\n".$this->getSQLRequest()."\n[SQLSTATE: ".$error[0].'][DriverCode: '.$error[1].'] => '.$error[2]."\n");
}
}
?>