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"); } } ?>