Login   Register  
PHP Classes
elePHPant
Icontem

File: BaseObject.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Indrek Altpere  >  ORM mapping class(like Hibernate), maps database table rows to objects  >  BaseObject.php  >  Download  
File: BaseObject.php
Role: Class source
Content type: text/plain
Description: the magical class that makes so many things possible
Class: ORM mapping class(like Hibernate), maps database table rows to objects
Store and retrieve objects in MySQL table records
Author: By
Last change: Some old code cleanup.
CloneMe cleans up id field value to allow Save/Create actually create new instance.
FlushObjects accepts BaseObject instance as optional argument and cleans up internal references of that type.
Overridable CreateReloads function that enables/disables full data reloading from database when data is inserted (default true).
Overridable SaveReloads function that enables/disables full data reloading from database when data is updated (default true).
Reload function that refreshes all field data from db.
Overridable AllowIdSetting function that enables/disables setting of object id value with $obj->id = 123;
Improved GetById function: the extending class must pass __CLASS__ as second parameter for better performance.
New static function DoSearch that can be called from extending class similarily, it also uses cache system whenever possible to return existing objects in memory, also refreshes any one already in memory.
New function IsDirty that returns if any field values has been changed after loading and before any save/create action.
New function Reset that clears any changes done to the object (returns to the state it was when loaded from db).
New function GetAllObjects that accepts WHERE part and returns found objects (uses cache system if allowed).
New static function EnableCaching that disables/enables all caching for all objects (by default caching is allowed), useful when doing heavy lifting with many one-time objects and memory usage may become of an issue with thousands of objects in memory.

Overridable trigger actions:
BeforeSave()
AfterSave($edits, $before)
BeforeCreate()
AfterCreate()
BeforeDelete()
AfterDelete()

NB! All extending classes must change the ById definition from
public static function &ById($id)
to
public static function ById($id)
And contents are advisable to change from
return self::GetById($id);
to
return self::GetById($id, __CLASS__);
Date: 2 years ago
Size: 40,271 bytes
 

Contents

Class file image Download
<?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;
	}

	/**
	 * If Create should reload info back from database
	 * @return boolean 
	 */
	public function CreateReloads() {
		return true;
	}

	/**
	 * If Save should reload info back from database
	 * @return boolean 
	 */
	public function SaveReloads() {
		return true;
	}

	protected static $_enableCaching = true;

	public static function EnableCaching($enable = true) {
		self::$_enableCaching = !!$enable;
	}

	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
	 */
	private $_data = array();

	/**
	 * New data to be saved to database stored as array
	 * @var array
	 */
	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;
	}

	/**
	 * Resets/clears any changes that are done after object creation/loading
	 */
	public function Reset() {
		$this->_newdata = array();
	}

	/**
	 * 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;
	}

	/**
	 * Returns if this instance has changed field values and needs saving to keep DB synchronized
	 *
	 * @return bool If any of the values has been modified and is out of sync with DB
	 */
	public function IsDirty() {
		return count($this->_newdata) > 0;
	}

	/**
	 * Loads object from database by id
	 *
	 * @param integer $id Id of the record in database
	 */
	public function __construct($id = null, $optimalSelect = false) {
		if (is_array($id)) {
			$this->___construct_from_array($id, $optimalSelect);
			return;
		}
		//do not load item info when id field is empty or NULL or if it's numerical 0 (allow id's that are strings)
		if (!is_null($id) && $id !== '' && (!is_numeric($id) || $id > 0)) {
			$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, $optimalSelect = false) {
		$where = self::QueryFromArray($arr);
		$idstr = $this->IdField();
		if ($optimalSelect) {
			$arr = Mysql::GetArray("SELECT * FROM `" . $this->GetTableName() . "` WHERE $where LIMIT 1");
			if ($arr[$idstr]) {
				$this->Load($arr);
				if (!self::$_enableCaching || !$this->allowCaching)
					return;
				if (!self::HaveObjectCached($this, $arr[$idstr])) {
					self::CacheObject($this);
					return;
				}
				$existing = self::GetObject($this, $arr[$idstr]);
				if ($existing) {
					$this->_newdata = & $existing->_newdata;
					$this->_data = & $existing->_data;
				}
			}
			return;
		}
		$row = Mysql::GetRow("SELECT `$idstr` FROM `" . $this->GetTableName() . "` WHERE $where LIMIT 1");
		if (isset($row[0])) {
			$this->LoadById($row[0]);
		}
	}

	private static $allowedChecks = array('=', '!=', '<', '>', '<=', '>=');

	/**
	 * Builds query from fields array
	 * @param array $arr Field => value array
	 * @param enum $mode Possible values AND|OR which define what logical operator is to be used for fields
	 * @return <type>
	 */
	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) == 1)
					$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)) {
					//allow parenthesis (functions on fields), for example to do WHERE DATE(modified_at)="2011-01-01"
					if (strpos($key, ')') !== false && strpos($key, '(') !== false)
						$where[] = $key . $modifier . '"' . Mysql::EscapeString($value) . '"';
					else
						$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, $refresh = false) {
		if (!is_array($arr)) {
			$arr = array();
		}
		$this->_data = $arr;
		if (!$refresh)
			$this->_newdata = array();
		else {
			//when refreshing obj from database, remove any newdata key-value pairs that already exist in the _data
			$newdata = $this->_newdata;
			foreach ($newdata as $k => $v)
				if ($this->_data[$k] . '' === $v . '')
					unset($this->_newdata[$k]);
		}
	}

	/**
	 * 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 (!self::$_enableCaching || !$this->AllowCaching()) {
			$this->Load(Mysql::GetArray('SELECT * FROM `' . $this->GetTableName() . '` WHERE `' . $this->IdField() . '`="' . Mysql::EscapeString($id) . '"'));
		} else {
			$existing = self::GetObject($this, $id);
			if ($existing) {
				$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;
		}
	}

	protected static function DoSearch($search, $className) {
		if (is_object($search)) {
			trigger_error('Providing object as search argument is not supported!');
			return null;
		}
		if (!$className || !is_string($className) || !is_subclass_of($className, __CLASS__)) {
			trigger_error('Cannot do search for provided classname: ' . $className);
			return null;
		}
		/* @var $obj BaseObject */
		$obj = new $className();
		$tableName = $obj->GetTableName();
		//at first check for object existence in internal cache
		if (!is_array($search)) {
			if (isset(self::$objects[$tableName][$search]))
				return self::$objects[$tableName][$search];
			$search = array($obj->IdField() => $search);
		}
		$where = self::QueryFromArray($search);
		$idstr = $obj->IdField();
		$arr = Mysql::GetArray("SELECT * FROM `$tableName` WHERE $where LIMIT 1");
		if (!$arr)
			return null;
		$cacheAllowed = self::$_enableCaching && $obj->AllowCaching();
		$obj_id = $arr[$idstr];
		/* @var $match BaseObject */
		$match = null;
		//found the object
		if ($obj_id) {
			if (!isset(self::$objects[$tableName]))
				self::$objects[$tableName] = array();
			$wasCached = isset(self::$objects[$tableName][$obj_id]);
			//if we have it cached, use that instance BUT refresh the data in that instance
			if ($wasCached) {
				$match = self::$objects[$tableName][$obj_id];
				$match->Load($arr, true);
			} else {
				//otherwise when it was not cached, fill the proxy instance with data and cache it when allowed
				$obj->Load($arr);
				$match = $obj;
				if ($cacheAllowed)
					self::$objects[$tableName][$obj_id] = $match;
			}
		}
		return $match;
	}

	/**
	 * 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, $classname = null) {
		if ($id === false || is_null($id) || is_array($id) || $id === '')
			return null;
		// class name not provided or wrong, use more advanced methods
		if ($classname === null || !is_string($classname) || !is_subclass_of($classname, __CLASS__)) {
			trigger_error('Calling GetById without providing correct classname is deprecated, provided was: ' . $classname);
			$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) {
		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);
	}

	/**
	 * Contains array of fields allowed to be changed
	 *
	 * @var array
	 */
	private $settablefields = null;

	protected function AllowIdSetting() {
		return false;
	}

	/**
	 * 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)) {
				if (!$this->AllowIdSetting())
					$unsettablefields[] = $this->IdField();
				$flipped = array_flip($settablefields);
				foreach ($unsettablefields as $unsettablefield) {
					if (in_array($unsettablefield, $settablefields, true)) {
						unset($settablefields[$flipped[$unsettablefield]]);
					}
				}
			} else {
				$flipped = array_flip($settablefields);
				if (!$this->AllowIdSetting() && isset($flipped[$this->IdField()])) {
					unset($settablefields[$flipped[$this->IdField()]]);
				}
			}

			$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;
	}

	public function Reload($ignoreUnsavedData = false) {
		$id = $this->IdValue();
		if ($id) {
			if (!$ignoreUnsavedData && $this->IsDirty()) {
				trigger_error('Reloading changed object caused loss of unsaved data!');
			}
			$arr = Mysql::GetArray('SELECT * FROM `' . $this->GetTableName() . '` WHERE `' . $this->IdField() . '`="' . Mysql::EscapeString($id) . '"');
			if ($arr)
				$this->Load($arr);
			else
				trigger_error('Failed to reload ' . $this . ' from db!');
		} else {
			trigger_error('Cannot reload object without id: ' . $this);
		}
	}

	/**
	 * 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 new value does not equal to the one fetched from db (or does not exist in newly created object) set it to _newdata array
			if (!array_key_exists($prop_name, $this->_data) || $this->_data[$prop_name] . '' !== $prop_value . '') {
				$this->_newdata[$prop_name] = $prop_value;
				//if value existed and was equal to the data fetched from db, unset it from _newdata array, allows improved tracking with IsDirty and may save some db calls
			} elseif (isset($this->_newdata[$prop_name])) {
				unset($this->_newdata[$prop_name]);
			}
			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
	 * @param bool $force Whether to skip field checks in database and just set the field value
	 */
	protected function Set($prop_name, $prop_value, $force = false) {
		if ($force || array_key_exists($prop_name, $this->GetAllFieldsDBData())) {
			if (!array_key_exists($prop_name, $this->_data) || $this->_data[$prop_name] !== $prop_value) {
				$this->_newdata[$prop_name] = $prop_value;
			}
			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->IsDirty()) {
			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);
		}
		$od = array();
		foreach ($savefields as $k)
			$od[$k] = $this->_data[$k];
		$this->BeforeSave();
		if (Mysql::Query('UPDATE `' . $this->GetTableName() . '`' . $this->GenerateSql($savefields, $this->GetData(), $this->GetAllFieldsDBData(), $idstr) . ' WHERE `' . $idstr . '`="' . Mysql::EscapeString($id) . '"')) {
			$this->_data = array_merge($this->_data, $this->_newdata);
			$nd = $this->_newdata;
			$this->_newdata = array();
			if (self::$_save_reloads && $this->SaveReloads()) {
				$this->Load(Mysql::GetArray('SELECT * FROM `' . $this->GetTableName() . '` WHERE `' . $idstr . '`="' . Mysql::EscapeString($id) . '"'));
			}
			$this->AfterSave($nd, $od);
			return true;
		}
		return false;
	}

	/**
	 * Overridabe trigger
	 */
	protected function BeforeSave() {
		
	}

	/**
	 * Overridabe trigger
	 */
	protected function AfterSave($edits, $before) {
		
	}

	/**
	 * Overridabe trigger
	 */
	protected function BeforeCreate() {
		
	}

	/**
	 * Overridabe trigger
	 */
	protected function AfterCreate() {
		
	}

	/**
	 * Overridabe trigger
	 */
	protected function BeforeDelete() {
		
	}

	/**
	 * Overridabe trigger
	 */
	protected function AfterDelete() {
		
	}

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

	/**
	 * Inserts new record of object to database
	 *
	 * @param boolean $force Whether to force the creation of new record or not (will insert new value, or clone existing object)
	 * @return integer New id of object
	 */
	public function Create($force = false) {
		$idstr = $this->IdField();
		$keys = array();
		if (!$force) {
			$keys = array_keys($this->_newdata);
		} else {
			$arr = $this->GetData();
			if (isset($arr[$this->IdField()]) && !$this->AllowIdSetting()) {
				unset($arr[$this->IdField()]);
			}
			$keys = array_keys($arr);
			unset($arr);
		}
		$this->BeforeCreate();
		$sql = 'INSERT INTO `' . $this->GetTableName() . '`' . $this->GenerateSql($keys, $this->GetData(), $this->GetAllFieldsDBData(), $idstr);
		if (Mysql::Query($sql)) {
			$id = Mysql::InsertId();
			if (self::$_create_reloads && $this->CreateReloads()) {
				$this->Load(Mysql::GetArray('SELECT * FROM `' . $this->GetTableName() . '` WHERE `' . $idstr . '`="' . Mysql::EscapeString($id) . '"'));
			} else {
				$data = $this->GetData();
				$data[$idstr] = $id;
				$this->Load($data);
			}
			self::CacheObject($this);
			$this->AfterCreate();
			return $this->IdValue();
		}
		return false;
	}

	/**
	 * 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 (!self::$_enableCaching || !$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])) {
			$arr = Mysql::GetArray('SELECT * FROM `' . $tablename . '` WHERE `' . $idstr . '`="' . Mysql::EscapeString($id) . '"');
			if (!$arr)
				return null;
			$newobj = $obj->GetNew();
			$newobj->Load($arr);
			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
	 *
	 * @return int Number of objects flushed from cache
	 */
	public static function FlushObjects(BaseObject $obj = null) {
		if ($obj) {
			if (isset(self::$objects[$obj->GetTableName()]))
				$ret = self::UnsetRecursive(self::$objects[$obj->GetTableName()]);
			else
				$ret = 0;
			self::$objects[$obj->GetTableName()] = array();
			return $ret;
		}
		$ret = self::UnsetRecursive(self::$objects);
		self::$objects = array();
		return $ret;
	}

	private static function UnsetRecursive(&$arr) {
		$count = 0;
		foreach ($arr as $k => &$v) {
			if (is_array($v)) {
				$count += self::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 = array();
		$newclass->_newdata = $this->GetData();
		unset($newclass->_newdata[$this->IdField()]);
		return $newclass;
	}

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

	/**
	 * 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;
	}

	/**
	 * Shorthand for GetByIds(GetIds($where)) logic
	 * @param string $where narrowing sql command
	 * @return array
	 */
	public function &GetAllObjects($where = '') {
		return $this->GetByIds(Mysql::GetRows('SELECT `' . $this->IdField() . '` FROM `' . $this->GetTableName() . '` ' . $where, true));
	}

	/**
	 * 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) {
		$queryids = array();
		$objs = array();
		$tableName = $this->GetTableName();
		foreach ($ids as $id) {
			if (isset(self::$objects[$tableName][$id])) {
				$objs[$id] = self::$objects[$tableName][$id];
			} else {
				$queryids[] = $id;
			}
		}
		$loadedobjs = array();
		$idstr = $this->IdField();
		if (count($queryids)) {
			$allowcaching = $allowcaching && self::$_enableCaching && $this->AllowCaching();
			if (!isset(self::$objects[$tableName]))
				self::$objects[$tableName] = array();
			$where = 'WHERE `' . $idstr . '` IN("' . join('","', $queryids) . '")';
			$res = Mysql::GetArrays('SELECT * FROM `' . $tableName . '` ' . $where);
			foreach ($res as &$objarr) {
				$obj = $this->GetNew();
				$obj->Load($objarr);
				$loadedobjs[$obj->$idstr] = $obj;
				if ($allowcaching) {
					self::$objects[$tableName][$obj->$idstr] = $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
	 */
	public 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 (self::$_enableCaching && $obj->AllowCaching()) {
			$tablename = $obj->GetTableName();
			if (!isset(self::$objects[$tablename])) {
				self::$objects[$tablename] = array();
			}
			self::$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;
	}

	/**
	 * Private helper function to build id=>name fetching query
	 * @param BaseObject $obj Object for which to fetch results from db
	 * @param string $idfield Name of the field to use for array key
	 * @param string $namefield Name of the field to use for array value
	 * @param string $where Additional WHERE clause to narrow down fields
	 * @param bool $collapse Whether to collaps results to array(id => name) or return as array(array('idfield' => id, 'valuefield' => value))
	 * @return array
	 */
	private static function &AllAsIdNameByObject(BaseObject $obj, $idfield = 'id', $namefield = 'name', $where = '', $collapse = true) {
		$sql = 'SELECT `' . Mysql::EscapeString($idfield) . '`,`' . Mysql::EscapeString($namefield) . '` FROM ' . $obj->GetTableName() . ' ' . $where . ' ORDER BY `' . Mysql::EscapeString($namefield) . '`';
		if (!$collapse)
			return Mysql::GetArrays($sql);
		$arr = Mysql::GetRows($sql);
		$arr2 = array();
		foreach ($arr as $row) {
			$arr2[$row[0]] = $row[1];
		}
		return $arr2;
	}

	/**
	 * Returns all records for this object as array
	 * @param string $idfield Name of the field to use for array key
	 * @param string $namefield Name of the field to use for array value
	 * @param string $where Additional WHERE clause to narrow down results
	 * @param bool $collapse Whether to collapse results to array(id => name) or return as array(array('idfield' => id, 'valuefield' => value))
	 * @return array
	 */
	public function &AllAsIdName($idfield = 'id', $namefield = 'name', $where = '', $collapse = true) {
		return self::AllAsIdNameByObject($this, $idfield, $namefield, $where, $collapse);
	}

	/**
	 * Magic function definition to help casting to PHP string and display relevant information of object
	 * @return string
	 */
	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);

	//public static function search($search);//comment in for newer projects
}

/**
 * Iterator class to loop over big amounts of baseobject instances returned from query without hogging up all memory
 * Very MySQL specific
 */
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);
	}

}