Location: PHPKode > scripts > ORM mapping class > orm-mapping-class/BaseObject.php
<?php
/**
 * @author Indrek Altpere
 * @copyright Indrek Altpere
 * @uses Mysql class
 * @see ErrorManager for convenient error logging
 *
 */

abstract class BaseObject implements IBaseObject {
	/**
	 * Returns mysql table name for this class
	 *
	 * @abstract string
	 * @return string
	 */
	abstract public function GetTableName();
	/**
	 * Returns fileds that are allowed to be updated
	 *
	 * @return array
	 */
	protected function GetSettableFields() {
		return false;
	}
	/**
	 * Returns fields that are not allowed to be set
	 *
	 * @return array Fieldnames that are not allowed to be set
	 */
	protected function GetUnsettableFields() {
		return false;
	}
	protected $allowCaching = true;
	/**
	 * Returns if caching is allowed for this object to reduce repeating queries to database
	 * Eases to convert to memcache or xcache extension
	 *
	 * @return boolean
	 */
	public function AllowCaching() {
		return $this->allowCaching;
	}
	/**
	 * Allows to change if caching of this class is allowed or not
	 *
	 * @param boolean $newval
	 */
	public function SetAllowCaching($newval = true) {
		$this->allowCaching = $newval ? true : false;
	}

	protected $allowDescriptionCaching = true;
	/**
	 * Returns if mysql table description caching is allowed for this object to reduce repeating queries to database
	 * Eases to convert to memcache or xcache extension
	 *
	 * @return boolean
	 */
	public function AllowDescriptionCaching() {
		return $this->allowDescriptionCaching;
	}
	/**
	 * Allows to change if caching of this table's description in database is allowed or not
	 *
	 * @param boolean $newval
	 */
	public function SetAllowDescriptionCaching($newval = true) {
		$this->allowDescriptionCaching = $newval ? true : false;
	}

	/**
	 * Returns new instance of this class
	 *
	 * @return BaseObject
	 */
	public function GetNew($id = null) {
		$classname = get_class($this);
		return new $classname($id);
	}

	/**
	 * Returns the name of the main id field for this table
	 * Can be overriden in base classes to provide name for an id field different from 'id' (like 'userid' for example)
	 *
	 * @return string
	 */
	public function IdField() {
		return 'id';
	}

	/**
	 * Simple wrapper to get the primary id field value
	 *
	 * @return mixed
	 */
	public function IdValue() {
		$idstr = $this->IdField();
		return $this->$idstr;
	}

	/**
	 * Data retrieved from database stored as array
	 *
	 * @var array
	 * @access private
	 */
	private $_data = array();
	/**
	 * New data to be saved to database stored as array
	 *
	 * @var array
	 * @access private
	 */
	private $_newdata = array();

	/**
	 * Specifies if Create commands automatically reloads all data from database
	 *
	 * @var boolean
	 */
	private static $_create_reloads = true;
	/**
	 * Specifies if Save commands automatically reloads all data from database
	 *
	 * @var boolean
	 */
	private static $_save_reloads = false;
	const E_NONE = 0;
	const E_NORMAL = 1;
	const E_STRICT = 2;

	/**
	 * If all should be in debug mode
	 *
	 * @var int Error code
	 */
	private static $Debug = self::E_NORMAL;

	/**
	 * Sets error level for all things in BaseObject
	 * Valid values are one of the BaseObject::E_NONE, BaseObject::E_NORMAL, BaseObject::E_STRICT
	 *
	 * @param int $errorlvl Error level
	 */
	public static function SetDebug($errorlvl = 0) {
		if(!in_array($errorlvl, array(self::E_NONE, self::E_NORMAL, self::E_STRICT), true)) {
			$errorlvl = 0;
		}
		self::$Debug = $errorlvl;
	}

	/**
	 * Sets whether Create command reloads all data from database
	 *
	 * Used mainly for optimizing lots of Create commands executed in a row,
	 * when after execution there is no other info needed other than if creation succeeded
	 *
	 * @param boolean $reloads
	 */
	public static function SetCreateReloads($reloads = true) {
		self::$_create_reloads = $reloads ? true : false;
	}

	/**
	 * Sets whether Save command reloads all data from database
	 *
	 * Used for optimizing lots of Save commands executed in a row,
	 * when after execution there is no other info needed other than if creation succeeded
	 *
	 * @param boolean $reloads
	 */
	public static function SetSaveReloads($reloads = true) {
		self::$_save_reloads = $reloads ? true : false;
	}

	/**
	 * Returns array of old data plus new data to get most up to date data
	 *
	 * Used will represent the object as it will be after saving
	 *
	 * @return array
	 */
	public function GetData() {
		return array_merge($this->_data, $this->_newdata);
	}

	/**
	 * Returns data as it was retrieved from database in the first place
	 *
	 * Used to get default values before anything was overwritten with $obj->fieldname = $val;
	 *
	 * @return array
	 */
	public function GetOldData() {
		return $this->_data;
	}

	/**
	 * Returns fields that have changed after loading from database and before storing back to database
	 *
	 * Used to get values that were overwritten with $obj->fieldname = $val;
	 *
	 * @return array
	 */
	public function GetNewData() {
		return $this->_newdata;
	}

	/**
	 * Loads object from database by id
	 *
	 * @param integer $id Id of the record in database
	 */
	public function __construct($id = null) {
		if (is_array($id)) {
			if(func_num_args() > 1) {
				$args = func_get_args();
				$this->___construct_from_array($args);
			} else {
				$this->___construct_from_array($id);
			}
			return;
		}
		if(!is_null($id)) {
			$this->LoadById($id);
		}
	}

	/**
	 * Special constructor used for querying the data from database with a complex query
	 *
	 * array('fieldname' => '1') mean query asks data from table where fieldname equals 1
	 * special building or array: add separator ` to the end of the fieldname and append with one of the next values: =, !=, <, >, <=, >= (if not set, defaults to = )
	 * for example array('field1' => 1, 'field2`!=' => 2, 'field3`>' => 4) executes query where field1 = 1 AND field2 != 2 AND field3 > 4
	 * to use OR/AND queries specifically subarrays with keys OR/AND are needed plus you can use ! modifier in front of them
	 * like '!OR' => array('field1' => 1, 'field2' => 2) would be WHERE !(field1 = 1 OR field2 = 2)
	 *
	 * @param array $arr
	 */
	private function ___construct_from_array(array &$arr) {
		$where = self::QueryFromArray($arr);
		$idstr = $this->IdField();
		$row = Mysql::GetRow("SELECT `$idstr` FROM `".$this->GetTableName()."` WHERE $where");
		if(isset($row[0])) {
			$this->LoadById($row[0]);
		}
	}

	private static $allowedChecks = array('=', '!=', '<', '>', '<=', '>=');
	public static function QueryFromArray(array &$arr, $mode = 'AND') {
		$qprep = '';
		if(substr($mode, 0, 1) == '!') {
			$qprep = '!';
			$mode = substr($mode, 1);
		}
		if(!in_array(strtoupper($mode), array('AND', 'OR'))) {
			$mode = 'AND';
		}
		$where = array();
		foreach ($arr as $key => $value ) {
			if(is_array($value)) {
				$where[] = self::QueryFromArray($value, $key);
			} else {
				$fieldarr = explode('`', $key);
				if(count($fieldarr) == 0) $fieldarr[] = '=';
				//if was like !=`fieldname
				if(in_array($fieldarr[0], self::$allowedChecks)) {
					$key = $fieldarr[1];
					$modifier = $fieldarr[0];
				//was like fieldname`!=
				} else {
					$key = $fieldarr[0];
					$modifier = $fieldarr[1];
				}
				//not allowed modifier, default to =
				if(!in_array($modifier, self::$allowedChecks)) {
					$modifier = '=';
				}
				if(!is_null($value)) {
					$where[] = '`' . $key . '`'.$modifier.'"'.Mysql::EscapeString($value).'"';
				} else {
					if($modifier == '=') {
						$where[] = '`' . $key . '` IS NULL';
					} else {
						$where[] = '`' . $key . '` IS NOT NULL';
					}
				}
			}
		}
		return $qprep.'('.join(' '.$mode.' ', $where).')';
	}

	/**
	 * Populate data array by array retrieved from mysql query
	 *
	 * @param array $arr Array of data
	 */
	public function Load($arr) {
		if(!is_array($arr)) {
			$arr = array();
		}
		$this->_data = $arr;
		$this->_newdata = array();
	}

	/**
	 * Loads object data from database into this object
	 * Use Get instead to load from cache
	 * @see Get
	 *
	 * @param integer $id Id of the record in database
	 */
	public function LoadById($id) {
		if(!$this->AllowCaching()) {
			$this->Load(Mysql::GetArray('SELECT * FROM `'.$this->GetTableName().'` WHERE `'.$this->IdField().'`="'.Mysql::EscapeString($id).'"'));
		} else {
			$existing =& self::GetObject($this, $id);
			$this->changed =& $existing->changed;
			$this->_newdata =& $existing->_newdata;
			$this->_data =& $existing->_data;
		}
		return true;
	}

	/**
	 * Generic getter that returns the value from the data array instead of object's property
	 *
	 * @param string $prop_name
	 * @return mixed
	 */
	public function __get($prop_name) {
		if (array_key_exists($prop_name, $this->_newdata)) {
			return $this->_newdata[$prop_name];
		} elseif (array_key_exists($prop_name, $this->_data)) {
			return $this->_data[$prop_name];
		} else {
			return null;
		}
	}

	/**
	 * Returns object of type BaseObject
	 * NB! To implement it successfully since php does not allow static abstract functions,
	 * you MUST create a static public function &ById($id = null) { return parent::GetById($id); }
	 * Calling it otherwise ends up triggering error
	 *
	 * @param integer $id
	 * @return BaseObject
	 */
	protected static function &GetById($id) {
		$stack = debug_backtrace();
		//Should Be
		// [0] => Array ([file] => ...BaseObject.php, [line] => ..., [function] => GetById, [class] => BaseObject, [type] => ::, [args] => Array(...))
		// [1] => Array ([file] => ...ParentObject.php, [line] => ..., [function] => ById, [class] => ParentObject, [type] => ::, [args] => Array(...))
		$obj = null;
		if(count($stack) <= 1) {//was called directly
			$args = func_get_args();
			self::FireError('static function ById is not allowed to be called directly, only from a subclass', __FILE__, __LINE__, $args);
			return $obj;
		}
		$parent = $stack[0];
		$child = $stack[1];
		if(!$child['class'] || !is_subclass_of($child['class'], $parent['class'])) {
			$args = func_get_args();
			self::FireError('static function ById is not allowed to be called directly, only from a subclass by an overriding method calling parent::ById()', __FILE__, __LINE__, $args);
			return $obj;
		}
		$classname = $child['class'];
		$obj = new $classname();
		//fetch from storage
		return self::GetObject($obj, $id);
	}

	/**
	 * Triggers error if needed, also dies and outputs error directly if needed
	 *
	 * @param string $errorstr
	 * @param string $file
	 * @param string $line
	 * @param string $args
	 */
	private static function FireError($errorstr, $file, $line, $args) {
		echo $errorstr;
		if(self::$Debug >= self::E_NORMAL) {
			trigger_error($errorstr . " in $file at line $line with args (serialized form) ".serialize($args), E_USER_ERROR);
		}
		if(self::$Debug === self::E_STRICT) {
			die($errorstr . " in $file at line $line with args (serialized form) ".serialize($args));
		}
	}

	/**
	 * Creates and returns iterator class to iterate over large sets of object while preserving memory
	 *
	 * @param string $query Query string
	 * @param boolean $cancache Whether iterated objects can be cached or not
	 * @return BaseObjectIterator
	 */
	public function &GetIterator($query, $cancache = false) {
		return new BaseObjectIterator($query, $this, $cancache);
	}

	/**
	 * Marks if the object's data has changed
	 *
	 * @var boolean
	 */
	private $changed = false;

	/**
	 * Contains array of fields allowed to be changed
	 *
	 * @var array
	 */
	private $settablefields = null;
	/**
	 * Asks fields allowed to be changed from object
	 *
	 * @return array Fields allowed to be changed
	 */
	private function &TryGetSettableFields() {
		if(is_null($this->settablefields)) {
			$settablefields = $this->GetSettableFields();
			if($settablefields === false) {
				$settablefields = array_keys($this->GetAllFieldsDBData());
			}
			$unsettablefields = $this->GetUnsettableFields();
			if(is_array($unsettablefields) && count($unsettablefields)) {
				$unsettablefields[] = 'id';
				$flipped = array_flip($settablefields);
				foreach ($unsettablefields as $unsettablefield) {
					if(in_array($unsettablefield, $settablefields, true)) {
						unset($settablefields[$flipped[$unsettablefield]]);
					}
				}
			} else {
				$flipped = array_flip($settablefields);
				if(isset($flipped['id'])) {
					unset($settablefields[$flipped['id']]);
				}
			}

			$fields = array();
			$allfields = array_keys($this->GetAllFieldsDBData());
			foreach($settablefields as &$field) {
				if(in_array($field, $allfields, true)) {
					$fields[] = $field;
				}
			}
			$this->settablefields =& $fields;
		}
		return $this->settablefields;
	}

	/**
	 * Generic setter that allows to intercept and update _newdata array
	 * Returns true if setting succeeded
	 *
	 * @param string $prop_name Name of the field
	 * @param mixed $prop_value Value to set
	 * @return boolean Success
	 */
	public function __set($prop_name, $prop_value) {
		if(in_array($prop_name, $this->TryGetSettableFields(), true)) {
			if(!isset($this->_data[$prop_name]) || $this->_data[$prop_name].'' !== $prop_value.'') {
				$this->_newdata[$prop_name] = $prop_value;
				$this->changed = true;
			}
			return true;
		}
		return false;
	}

	/**
	 * Sets any existing field value regardless if that field is allowed to be publically edited or not
	 *
	 * @param string $prop_name Name of the property
	 * @param mixed $prop_value Value to set to
	 */
	protected function Set($prop_name, $prop_value) {
		if(array_key_exists($prop_name, $this->GetAllFieldsDBData())) {
			if($this->_data[$prop_name] !== $prop_value) {
				$this->_newdata[$prop_name] = $prop_value;
				$this->changed = true;
			}
			return true;
		}
		return false;
	}

	/**
	 * Saves object's changed fields to database
	 * Returns boolean values if saving failed or succeeded and integer value if object was new and was just created to database
	 *
	 * @param array Fields to save
	 * @return mixed Success/new id
	 */
	public function Save($fieldstosave = array()) {
		$idstr = $this->IdField();
		$id = $this->IdValue();
		if(!isset($id) || is_null($id)) {
			return $this->Create();
		}
		if(!$this->changed && $this->AllowCaching()) {
			return true;
		}
		$savefields = array();
		if(!is_array($fieldstosave) && !is_null($fieldstosave)) {
			$fieldstosave = array($fieldstosave);
		}
		if(count($fieldstosave)) {
			$savefields = $fieldstosave;
			$settable = array_keys($this->GetAllFieldsDBData());
			foreach ($savefields as $k => &$savefield) {
				if(!in_array($savefield, $settable, true)) {
					unset($savefields[$k]);
				}
			}
		} else {
			$savefields = array_keys($this->_newdata);
		}
		if(Mysql::Query('UPDATE `'.$this->GetTableName().'`'.$this->GenerateSql($savefields, $this->GetData(), $this->GetAllFieldsDBData(), $idstr).' WHERE `'.$idstr.'`="'.Mysql::EscapeString($id).'"')) {
			$this->LogAction($this, 'Save', __FILE__, __LINE__, $this->GetTableName(), array($this->$idstr));
			$this->_data = array_merge($this->_data, $this->_newdata);
			$this->_newdata = array();
			$this->changed = false;
			if(self::$_save_reloads) {
				$this->Load(Mysql::GetArray('SELECT * FROM `'.$this->GetTableName().'` WHERE `'.$idstr.'`="'.Mysql::EscapeString($id).'"'));
			}
			return true;
		}
		return false;
	}

	/**
	 * Deletes object from database
	 *
	 * @return boolean Succeess
	 */
	public function Delete() {
		$idstr = $this->IdField();
		$id = $this->IdValue();
		if(is_null($id)) {
			return false;
		}
		if(Mysql::Query('DELETE FROM `'.$this->GetTableName().'` WHERE `'.$idstr.'`="'.Mysql::EscapeString($id).'"')) {
			self::UnCacheObject($this);
			$this->LogAction($this, 'Delete', __FILE__, __LINE__, $this->GetTableName(), array($id));
			return true;
		}
		return false;
	}

	/**
	 * Inserts new record of object to database
	 *
	 * @return integer New id of object
	 */
	public function Create() {
		$idstr = $this->IdField();
		$sql = 'INSERT INTO `'.$this->GetTableName().'`'.$this->GenerateSql(array_keys($this->_newdata), $this->GetData(), $this->GetAllFieldsDBData(), $idstr);
		if(Mysql::Query($sql)) {
			$id = Mysql::InsertId();
			$this->LogAction($this, 'Create', __FILE__, __LINE__, $this->GetTableName(), array($id));
			if(self::$_create_reloads) {
				$this->Load(Mysql::GetArray('SELECT * FROM `'.$this->GetTableName().'` WHERE `'.$idstr.'`="'.Mysql::EscapeString($id).'"'));
			} else {
				$this->Load(array($idstr => $id));
			}
			self::CacheObject($this);
			return $this->IdValue();
		}
		return false;
	}

	/**
	 * Logs action to database, assuming there is global variable $user that is instance of BaseObject or global variable $userid to indicate the user who caused the logging action
	 *
	 * @param BaseObject $obj Object to log
	 * @param string $actionname Name of the action
	 * @param BaseUser $user User that did the action
	 * @param string $file File from where it was called
	 * @param integer $line Line number in that file
	 * @param string $table Name of the table
	 * @param mixed $ids Id/ids of affected objects
	 */
	private function LogAction(BaseObject $obj, $actionname, $file, $line, $table, array $ids) {
		global $user, $userid;
		if ($user instanceof BaseObject) {
			$userid = $user->IdValue();
		}
		$sql = 'INSERT INTO log_actions SET userid="'.$userid.
			'",action="'.Mysql::EscapeString($actionname).
			'",file="'.Mysql::EscapeString($file).
			'",line="'.Mysql::EscapeString($line).
			'",table="'.Mysql::EscapeString($table).
			'",ids="'.Mysql::EscapeString(join(',', $ids)).
			'",beforeedit="'.Mysql::EscapeString($obj->ToStringOld()).
			'",afteredit="'.Mysql::EscapeString($obj->ToString()).
			'"';
		if(false) {
			Mysql::Query($sql);
		}
	}

	/**
	 * Generates smart sql SET query by allowing to set NULL allowed fields to be set to NULL by setting property to '' or null
	 *
	 * @param array $fieldnames Names of the fields allowed to be added to sql
	 * @param array $arr Data
	 * @param array $fielddata Field descriptions
	 * @return string The sql SET query
	 */
	private static function GenerateSql($fieldnames, $arr, $fielddata = array(), $idstr = 'id') {
		$sql = array();
		foreach($fieldnames as $fieldname) {
			if(($arr[$fieldname] === '' || is_null($arr[$fieldname]) || !isset($arr[$fieldname])) && isset($fielddata[$fieldname]) && $fielddata[$fieldname]['isnullable']) {
				$sql[] = '`'.$fieldname.'`=NULL';
			} else {
				$sql[] = '`'.$fieldname.'`="'.Mysql::EscapeString($arr[$fieldname]).'"';
			}
		}
		if(count($sql)) {
			return ' SET '.join(',', $sql).' ';
		}

		return ' SET `'.$idstr.'`=`'.$idstr.'` ';
	}

	/**
	 * Objetcs storage
	 *
	 * @staticvar array Objects
	 * @access private
	 */
	private static $objects = array();
	/**
	 * Gets object from object storage if possible
	 *
	 * @param BaseObject $obj Object, of which type objects are to be returned
	 * @param integer $id Id of the object
	 * @return BaseObject
	 */
	public static function &GetObject(BaseObject $obj, $id) {
		//if $obj does not allow caching, always load it fresh
		if(!$obj->AllowCaching()) {
			return $obj->GetNew($id);
		}
		$tablename = $obj->GetTableName();
		//no such subarray for that table, create it
		if(!isset(self::$objects[$tablename])) {
			self::$objects[$tablename] = array();
		}
		$idstr = $obj->IdField();
		//no such object, load it
		if(!isset(self::$objects[$tablename][$id])) {
			$newobj = $obj->GetNew();
			$newobj->Load(Mysql::GetArray('SELECT * FROM `'.$tablename.'` WHERE `'.$idstr.'`="'.Mysql::EscapeString($id).'"'));
			self::$objects[$obj->GetTableName()][$id] =& $newobj;
		}
		return self::$objects[$tablename][$id];
	}

	/**
	 * Returns if object is cached currently or not
	 *
	 * @param BaseObject $obj
	 * @param integer $id
	 * @return boolean
	 */
	public static function HaveObjectCached(BaseObject $obj, $id = null) {
		if(!isset(self::$objects[$obj->GetTableName()])) {
			return false;
		}
		if(is_null($id)) $id = $obj->IdValue();
		if(!isset(self::$objects[$obj->GetTableName()][$id])) {
			return false;
		}
		return true;
	}

	/**
	 * Clears object storage
	 *
	 */
	public static function FlushObjects() {
		self::UnsetRecursive(BaseObject::$objects);
		BaseObject::$objects = array();
	}

	private static function UnsetRecursive(&$arr) {
		$count = 0;
		foreach ($arr as $k => &$v) {
			if(is_array($v)) {
				$count += BaseObject::UnsetRecursive($v);
			} else {
				unset($v);
				unset($arr[$k]);
				$count++;
			}
		}
		return $count;
	}

	/**
	 * Gets instance of this object from storage if possible
	 *
	 * @param integer $id Id of the object
	 * @return BaseObject
	 */
	public function &Get($id) {
		return self::GetObject($this, $id);
	}

	/**
	 * Returns clone of this class that has same values but is another instance
	 *
	 * @return BaseObject Clone
	 */
	public function &CloneMe() {
		$newclass = $this->GetNew();
		$newclass->_data = $this->_data;
		$newclass->_newdata = $this->_newdata;
		$newclass->changed = $this->changed;
		return $newclass;
	}

	/**
	 * Clears all data
	 *
	 */
	public function ClearData() {
		$this->_data = array();
		$this->_newdata = array();
		$this->changed = false;
	}

	/**
	 * Table field description storage
	 *
	 * @staticvar array
	 */
	private static $allfields = array();
	//field | type | null | key | default | extra
	/**
	 * Tries to load description data from storage if possible and returns it
	 *
	 * @param BaseObject $obj
	 * @return array
	 */
	private static function TryLoadAllFieldsDBData(BaseObject &$obj) {
		if(!$obj->AllowDescriptionCaching()) {
			$arrs = Mysql::GetColumnDataForTable($obj->GetTableName());
			$fields = array();
			foreach($arrs as $arr) {
				$fields[] = reset($arr);
			}
			unset($arrs);
			return $fields;
		}
		$tablename = $obj->GetTableName();
		//if such table data not stored yet, store it
		if(!isset(self::$allfields[$tablename])) {
			$arrs = Mysql::GetColumnDataForTable($tablename);
			$fields = array();
			foreach($arrs as $arr) {
				$fields[reset($arr)] = self::MysqlExplainRowToArray($arr);
			}
			self::$allfields[$tablename] = &$fields;
			unset($arrs);
		}
		return self::$allfields[$tablename];
	}

	public function GetAllFieldsDBData() {
		return self::TryLoadAllFieldsDBData($this);
	}

	/**
	 * Gets field DB description
	 *
	 * Using fieldname as fieldname1|fieldproperty will return the fieldroperty of the field
	 *
	 * @param string $fieldname Name of the field to get data for
	 * @return mixed Value as array or specific value of wanted array
	 */
	public function GetFieldDBData($fieldname) {
		$arr = self::TryLoadAllFieldsDBData($this);
		$keys = explode('|', $fieldname);
		$ret =& $arr;
		foreach($keys as $key) {
			$ret =& $ret[$key];
		}
		return $ret;
	}

	/**
	 * Convert row from mysql's EXPLAIN tablename; query to php array
	 *
	 * @param array $data
	 * @return array Better table description management in php array form
	 */
	private static function MysqlExplainRowToArray(array $data) {
		$arr = array();
		$arr['field'] = $data['Field'];
		$type = $arr['type'] = $data['Type'];
		$gottype = false;
		$arr2 = array();
		$gottype |= ($arr2['isint'] = strtolower(substr($type, 0, 3)) === 'int');
		if(!$gottype)
		$gottype |= ($arr2['isint'] = strtolower(substr($type, 0, 7)) === 'tinyint');
		if(!$gottype)
		$gottype |= ($arr2['isenum'] = strtolower(substr($type, 0, 4)) === 'enum');
		if(!$gottype)
		$gottype |= ($arr2['isdouble'] = strtolower(substr($type, 0, 6)) === 'double');
		if(!$gottype)
		$gottype |= ($arr2['isvarchar'] = strtolower(substr($type, 0, 7)) === 'varchar');
		if(!$gottype)
		$gottype |= ($arr2['isdatetime'] = strtolower($type) === 'datetime');
		if(!$gottype)
		$gottype |= ($arr2['isdate'] = strtolower($type) === 'date');
		if(!$gottype)
		$gottype |= ($arr2['istext'] = strtolower(substr($type, 0, 4)) === 'text');
		foreach ($arr2 as $k => $v) {
			if($v) $arr[$k] = true;
		}
		$arr['isnullable'] = !(strtolower($data['Null']) === 'no');
		if(isset($arr['isenum']) && $arr['isenum']) {
			$enumkeys = array();
			@eval('$enumkeys = array('.substr($type, 5).';');
			$arr['enumkeys'] = $enumkeys;
		}
		$arr['comment'] = $data['Comment'];
		return $arr;
	}

	/**
	 * Serializes all data to string
	 *
	 * @return string
	 */
	public function ToString() {
		$idstr = $this->IdField();
		return serialize(array_merge(array($idstr => $this->$idstr), $this->_data));
	}

	/**
	 * Serialize old data (before updating) to string
	 *
	 * @return string
	 */
	public function ToStringOld() {
		return serialize($this->GetOldData());
	}

	/**
	 * Returns class unserialized from string
	 *
	 * @param string $string
	 * @return BaseObject
	 */
	public function &FromString($string) {
		$newobj = $this->GetNew();
		$newobj->Load(unserialize($string));
		return $newobj;
	}

	/**
	 * Gets objects by their id's smartly
	 * Uses cached objects if possible and does minimal queries to database to ask for new objects
	 *
	 * @param array $ids Array of ids from table
	 * @param string $where Where clause
	 * @param boolean $allowcaching Whether to allow caching (might be good for handling with lots of objects one time)
	 * @return array
	 */
	public function &GetByIds(array $ids, $allowcaching = true) {
		$arr = array();
		$queryids = array();
		$objs = array();
		foreach ($ids as $id) {
			$arr[$id] = self::HaveObjectCached($this, $id);
			if(!$arr[$id]) {
				$queryids[] = $id;
			} else {
				$objs[$id] = &$this->Get($id);
			}
		}
		$loadedobjs = array();
		$idstr = $this->IdField();
		if(count($queryids)) {
			$where = 'WHERE `'.$idstr.'` IN("'.join('","', $queryids).'")';
			$res = Mysql::GetArrays('SELECT * FROM `'.$this->GetTableName().'` '.$where);
			foreach ($res as &$objarr) {
				$obj = $this->GetNew();
				$obj->Load($objarr);
				$loadedobjs[$obj->$idstr] = $obj;
				if($allowcaching) {
					self::CacheObject($obj);
				}
				unset($obj);
			}
		}
		$returnobjs = array();
		foreach ($ids as $id) {
			if(array_key_exists($id, $objs)) {
				$returnobjs[$id] = &$objs[$id];
			} else {
				$returnobjs[$id] = &$loadedobjs[$id];
			}
		}
		return $returnobjs;
	}

	/**
	 * Gets id's from database by where query
	 *
	 * @param string $where
	 * @return array
	 */
	protected function &GetIds($where = '') {
		$idstr = $this->IdField();
		return Mysql::GetRows('SELECT `'.$idstr.'` FROM `'.$this->GetTableName().'` '.$where, true);
	}

	/**
	 * Caches object if object allows itself to be cached
	 *
	 * @param BaseObject $obj
	 * @return BaseObject
	 */
	public static function &CacheObject(BaseObject &$obj) {
		if($obj->AllowCaching()) {
			$tablename = $obj->GetTableName();
			if(!isset(BaseObject::$objects[$tablename])) {
				BaseObject::$objects[$tablename] = array();
			}
			BaseObject::$objects[$tablename][$obj->IdValue()] =& $obj;
		}
		return $obj;
	}

	/**
	 * Uncaches object if cached
	 *
	 * @param BaseObject $obj
	 */
	private static function UnCacheObject(BaseObject &$obj) {
		if(self::HaveObjectCached($obj)) {
			$tablename = $obj->GetTableName();
			unset(self::$objects[$tablename][$obj->IdValue()]);
		}
	}

	/**
	 * Converts input multilevel array to single array with wanted key value pairs
	 *
	 * @param array $arr Array to convert
	 * @param string $fieldname
	 * @param string $idfield
	 */
	public static function &ToIdValueArray(array &$arr, $fieldname = 'name', $idfield = 'id') {
		$retarr = array();
		if(reset($arr) instanceof BaseObject) {
			foreach ($arr as &$subarr) {
				$retarr[$subarr->$idfield] = $subarr->$fieldname;
			}
		} else {
			foreach ($arr as &$subarr) {
				$retarr[$subarr[$idfield]] = $subarr[$fieldname];
			}
		}
		return $retarr;
	}

	/**
	 * Converts multilevel array array(0=>array('id'=>1),1=>array('id'=>2),..) by $fieldname to array(1, 2, ..)
	 *
	 * @param array $arr Multilevel array to convert
	 * @param string $fieldname Name of the field to use for collapsing
	 */
	public static function &MultiLevelToSingleArray(array &$arr, $fieldname = 'id') {
		$retarr = array();
		foreach ($arr as &$subarr) {
			$retarr[] = $subarr[$fieldname];
		}
		return $retarr;
	}

	/**
	 * Returns class that is binded to table $tablename
	 *
	 * @param string $tablename
	 * @return BaseObject
	 */
	public static function &ClassByTableName($tablename) {
		$classname = self::ClassNameByTableName($tablename);
		return new $classname();
	}

	private static function EndsWith($str, $end) {
		return substr($str, -1 * strlen($end)) === $end.'';
	}

	private static $TableToClassMapping = array();
	private static $ProcessedClasses = array();
	/**
	 * Returns custom baseobject
	 *
	 * @param string $tablename
	 * @return BaseObject
	 */
	public static function &ClassNameByTableName($tablename) {
		$tablename = str_replace(array(' ',';', '"', "'", "\t", "\n"), array('', '', '', '', '', ''), $tablename);
		//find if table to class mapping has this tablename stored
		if(isset(self::$TableToClassMapping[$tablename])) {
			return self::$TableToClassMapping[$tablename];
		}
		//check declared classes in memory
		$classes = get_declared_classes();
		foreach ($classes as $class) {
			if(isset(self::$ProcessedClasses[$class])) {
				continue;
			}
			if(is_subclass_of($class, 'BaseObject')) {
				$classinst = new $class();
				$classtable = $classinst->GetTableName();
				self::$TableToClassMapping[$classtable] = $class;
				unset($classinst);
			}
			self::$ProcessedClasses[$class] = true;
		}
		//if class was found in memory return classname
		if(isset(self::$TableToClassMapping[$tablename])) {
			return self::$TableToClassMapping[$tablename];
		}
		//try class with same name as tablename
		if(class_exists($tablename) && is_subclass_of($tablename, 'BaseObject')) {
			self::$TableToClassMapping[$tablename] = $tablename;
			return $tablename;
		}
		//no luck with loaded classes try figuring out the class name by removing extra s or es or ies from end (if tablename reflects multitude of object and classname reflecs single object
		//for example quizes -> quiz, lorries -> lorry,
		$namelen = strlen($tablename);
		$origlen = $namelen;
		$appender = '';
		if(self::EndsWith($tablename, 'zes')) {
			$namelen -= 2;
		} elseif (self::EndsWith($tablename, 'ies')) {
			$namelen -= 3;
			$appender = 'y';
		} elseif (self::EndsWith($tablename, 's')) {
			$namelen -= 1;
		}
		if($origlen != $namelen) {
			$classname = substr($tablename, 0, $namelen) . $appender;
			if(class_exists($classname) && is_subclass_of($classname, 'BaseObject')) {
				$inst = new $classname();
				if($inst->GetTableName() == $tablename) {
					self::$TableToClassMapping[$tablename] = $classname;
					return $classname;
				}
			}
		}
		//try once again with es ending for outboxes -> outbox
		$namelen = $origlen;
		if(self::EndsWith($tablename, 'es')) {
			$namelen -= 2;
		}
		if($origlen != $namelen) {
			$classname = substr($tablename, 0, $namelen) . $appender;
			if(class_exists($classname) && is_subclass_of($classname, 'BaseObject')) {
				$inst = new $classname();
				if($inst->GetTableName() == $tablename) {
					self::$TableToClassMapping[$tablename] = $classname;
					return $classname;
				}
			}
		}
		//still no luck ? as last resort create dummy class to be used as simple data fetcher
		$classname = 'BaseObject_'.$tablename.md5($tablename);
		if(class_exists($classname, false)) {
			return $classname;
		}
		eval("class $classname extends BaseObject {public function GetTableName(){return '$tablename';}public function AllowCaching(){return false;}public static function &ById(\$id){return self::GetById(\$id);}}");
		return $classname;
	}

	private static function &AllAsIdNameByObject(BaseObject $obj, $idfield = 'id', $namefield = 'name') {
		$arr = Mysql::GetRows('SELECT `'.Mysql::EscapeString($idfield).'`,`'.Mysql::EscapeString($namefield).'` FROM '.$obj->GetTableName().' ORDER BY `'.Mysql::EscapeString($namefield).'`');
		$arr2 = array();
		foreach ($arr as $row) {
			$arr2[$row[0]] = $row[1];
		}
		return $arr2;
	}

	public function &AllAsIdName($idfield = 'id', $namefield = 'name') {
		return self::AllAsIdNameByObject($this, $idfield, $namefield);
	}

	public function __toString() {
		$idstr = $this->IdField();
		return get_class($this).'['.$idstr.'='.$this->$idstr.']';
	}
}

/**
 * Little trick to make the static function ById compulsory by making the php spit out fatal error if function is not implemented in base class.
 * Static functions can be declared in interfaces but not abstract classes, adding it to interface and making the abstract class implement the interfaces forces the extenting class effectively to implement it.
 *
 */
interface IBaseObject {
	public static function &ById($id);
}

class BaseObjectIterator implements SeekableIterator, Countable {
	private $mysqlResult = null;
	private $currentObj = null;
	private $index = 0;
	private $count = 0;
	private $cancache = false;
	private $query = null;
	/**
	 * @var BaseObject
	 */
	private $obj = null;
	public function __construct($result, $obj, $cancacheobjects = false) {
		//if query string, exequte query and store result
		if(is_string($result)) {
			$this->query = $result;
			$result = Mysql::Query($result);
		}
		$this->obj = is_string($obj) ? new $obj() : $obj;
		$this->mysqlResult = $result;
		$this->count = mysql_num_rows($result);
		$this->index = 0;
		$this->currentObj = null;
		$this->cancache = $cancacheobjects ? true : false;
	}
	public function seek($index) {
		$this->index = $index;
		return mysql_data_seek($this->mysqlResult, $index);
	}
	public function &next() {
		$arr = mysql_fetch_array($this->mysqlResult, MYSQL_ASSOC);
		$this->currentObj = $this->obj->GetNew();
		$this->currentObj->Load($arr);
		//is in cache, select object from cache
		if(BaseObject::HaveObjectCached($this->currentObj)) {
			$this->currentObj = BaseObject::GetObject($this->currentObj, $this->currentObj->IdValue());
		//not in cache but can be cached, cache it
		} elseif($this->cancache) {
			BaseObject::CacheObject($this->currentObj);
		}
		$this->index += 1;
		return $this->currentObj;
	}
	public function &current() {
		return $this->currentObj;
	}
	public function valid() {
		return $this->index < $this->count;
	}
	public function rewind() {
		mysql_data_seek($this->mysqlResult, 0);
		$this->currentObj = $this->next();
		$this->index = 0;
	}
	public function key() {
		return $this->index;
	}
	public function count() {
		return $this->count;
	}
	public function __destruct() {
		if($this->mysqlResult) {
			mysql_free_result($this->mysqlResult);
			$this->mysqlResult = null;
		}
	}
	public function __sleep() {
		$this->__destruct();
	}
	public function __wakeup() {
		if($this->query) {
			$this->mysqlResult = Mysql::Query($this->query);
			$this->count = mysql_num_rows($this->mysqlResult);
		}
		$old = $this->index;
		$this->seek($old);
		$this->currentObj = $this->next();
		$this->seek($old);
	}
}

?>
Return current item: ORM mapping class