<?php
/**
* @author Indrek Altpere
* @copyright Indrek Altpere
* @uses Mysql class
* @see ErrorManager for convenient error logging
*/
abstract class BaseObject implements IBaseObject {
/**
* @return string Mysql table name for this class
*/
abstract public function GetTableName();
/**
* @return array Fields that are allowed to be updated
*/
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/refresh info back from database, class specific
* @return bool
*/
public function CreateReloads() {
return true;
}
/**
* If Save should reload/refresh info back from database, class specific
* @return bool
*/
public function SaveReloads() {
return true;
}
protected static $_enableCaching = true;
/**
* Allows to disable/re-enable global caching, useful when dealing with lots of objects and memory usage is more critical than query count reduction
* @param bool
*/
public static function EnableCaching($enable = true) {
self::$_enableCaching = !!$enable;
}
/**
* If caching is allowed for this object to reduce repeating queries to database
* @return bool
*/
public function AllowCaching() {
return true;
}
/**
* @deprecated Per-instance caching is now enabled by default, use EnableCaching for globally switching cache off/on or override AllowCaching
*/
public function SetAllowCaching($newval = true) {
trigger_error('SetAllowCaching is deprecated, use static EnableCaching or override AllowCaching!');
}
/**
* 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 bool
*/
private static $_create_reloads = true;
/**
* Specifies if Save commands automatically reloads all data from database
* @var bool
*/
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, if set to false, NO Create command of ANY object is allowed to reload
* 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 bool $reloads
*/
public static function SetCreateReloads($reloads = true) {
self::$_create_reloads = !!$reloads;
}
/**
* Sets whether Save command reloads all data from database, if set to false, NO Save command of ANY object is allowed to reload
* Used for optimizing lots of Save commands executed in a row when after execution there is no other info needed other than if saving succeeded
* @param bool $reloads
*/
public static function SetSaveReloads($reloads = true) {
self::$_save_reloads = !!$reloads;
}
/**
* Returns array of old data plus new data to get current object state, represents the object as it will be after saving/creating
* @return array
*/
public function GetData() {
return array_merge($this->_data, $this->_newdata);
}
/**
* Returns data as it was retrieved from database in the first place, returns 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 to object after loading it
*/
public function Reset() {
$this->_newdata = array();
}
/**
* Returns fields that have changed after loading from database and before storing back to database, returns 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
* @param string $fieldname The field that should be checked for modification
* @return bool
*/
public function IsDirty($fieldname = null) {
if ($fieldname !== null)
return array_key_exists($fieldname, $this->_newdata);
return count($this->_newdata) > 0;
}
/**
* Loads object from database by id
* @param int $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 (!empty($id)) {
$this->LoadById($id);
}
}
/**
* Special constructor used for querying the data from database with a complex query
*
* array('fieldname' => '1') means 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 with 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;
//not cached ? try caching self
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]);
}
}
public static $triggerUnsavedChanges = false; //may cause too much warnings in some cases...
public function __destruct() {
//trigger allowed, we have changes and object exists in db (to avoid warnings for any kind of proxy/temp objects etc)
if (self::$triggerUnsavedChanges && $this->_newdata && $this->_data[$this->IdField()])
trigger_error('Destroying a BaseObject with unsaved changes: ' . $this . ' changes=' . print_r($this->_newdata, true));
}
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 ($value !== null) {
//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 int $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 {
//not in _newdata, which is checked against db, not in _data, which is checkec against db
//if we have db info loaded, check if field is valid
if ($this->HasFieldInDBData($prop_name) === false)
trigger_error('Field "' . $prop_name . '" seems to be missing from database table ' . $this->GetTableName() . ', instance=' . $this, E_USER_WARNING);
return null;
}
}
/**
* Generic isset/empty handler, needed because otherwise even after $obj->f = 1; the following call empty($obj->f); will still return true
* @param string $prop_name
* @return bool
*/
public function __isset($prop_name) {
if (array_key_exists($prop_name, $this->_newdata)) {
return isset($this->_newdata[$prop_name]);
} elseif (array_key_exists($prop_name, $this->_data)) {
return isset($this->_data[$prop_name]);
} else {
return false;
}
}
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 int $id
* @return BaseObject
*/
protected static function GetById($id, $classname = null) {
if (empty($id) || is_array($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 bool $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 per-table fields allowed to be changed
* @var array
*/
private static $settablefields = array();
protected function AllowIdSetting() {
return false;
}
/**
* Asks fields allowed to be changed from object
* @return array Fields allowed to be changed
*/
private function &TryGetSettableFields() {
$tablename = $this->GetTableName();
if (!isset(self::$settablefields[$tablename])) {
$settablefields = $this->GetSettableFields();
if ($settablefields === false) {
$settablefields = array_keys($this->GetAllFieldsDBData());
}
$unsettablefields = $this->GetUnsettableFields();
$idstr = $this->IdField();
if (is_array($unsettablefields) && count($unsettablefields)) {
if (!$this->AllowIdSetting())
$unsettablefields[] = $idstr;
$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[$idstr])) {
unset($settablefields[$flipped[$idstr]]);
}
}
$fields = array();
$allfields = array_keys($this->GetAllFieldsDBData());
foreach ($settablefields as &$field) {
if (in_array($field, $allfields, true)) {
$fields[] = $field;
}
}
self::$settablefields[$tablename] = &$fields;
}
return self::$settablefields[$tablename];
}
/**
* Reloads the object from database, overwriting all changes
* @param bool If existence of unsaved data should be ignored or if it should trigger a warning
*/
public function Reload($ignoreUnsavedData = false) {
if ($this->_deleted) {
trigger_error('Cannot reload deleted object: ' . $this);
return;
}
$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) . '" LIMIT 1');
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
* @param string $prop_name Name of the field
* @param mixed $prop_value Value to set
* @return bool 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 (array_key_exists($prop_name, $this->_newdata)) {
unset($this->_newdata[$prop_name]);
}
return true;
}
trigger_error('Field "' . $prop_name . '" seems to be missing from database table ' . $this->GetTableName() . ', instance=' . $this, E_USER_WARNING);
return false;
}
/**
* Sets any existing field value regardless if that field is allowed to be publicly 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;
//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 (array_key_exists($prop_name, $this->_newdata)) {
unset($this->_newdata[$prop_name]);
}
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, empty array means all changes are saved
* @return bool|int
*/
public function Save($fieldstosave = array()) {
if ($this->_deleted) {
trigger_error('Cannot save already deleted object: ' . $this);
return false;
}
$idstr = $this->IdField();
$id_old = $this->_data[$idstr];
$id_new = $this->_newdata[$idstr];
$id = array_key_exists($idstr, $this->_newdata) ? $id_new : $id_old;
//no ID value, redirect to Create
if (empty($id) || $this->AllowIdSetting() && empty($id_old)) {
return $this->Create();
}
$this->_wasCreated = false;
//nothing changed, do nothing and report success
if (!$this->IsDirty()) {
return true;
}
$savefields = array();
if (!is_array($fieldstosave) && $fieldstosave !== null) {
$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 {
//no fields given, just use changed fields
$savefields = array_keys($this->_newdata);
}
$od = array();
foreach ($savefields as $k)
$od[$k] = $this->_data[$k];
//newdata does not have id set, no id change occurred
if (!$id_new)
$id_new = $id_old;
$this->BeforeSave();
$tablename = $this->GetTableName();
if (Mysql::Query('UPDATE `' . $tablename . '`' . $this->GenerateSql($savefields, $this->GetData(), $this->GetAllFieldsDBData(), $idstr) . ' WHERE `' . $idstr . '`="' . Mysql::EscapeString($id_old) . '"')) {
$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 `' . $tablename . '` WHERE `' . $idstr . '`="' . Mysql::EscapeString($id_new) . '"'));
}
//id changed?
if ($id_new != $id_old) {
//We need to delete the OLD ID reference
if (isset(self::$objects[$tablename][$id_old]))
unset(self::$objects[$tablename][$id_old]);
self::CacheObject($this);
}
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() {
}
private $_deleted = false;
/**
* @return bool If the current in-memory object has been deleted from database
*/
public function IsDeleted() {
return $this->_deleted;
}
/**
* Deletes object from database
* @return bool If succeeded
*/
public function Delete() {
if ($this->_deleted) {
trigger_error('Cannot delete already deleted object: ' . $this);
return false;
}
$idstr = $this->IdField();
$id = $this->IdValue();
//no ID value, nothing to delete
if (empty($id)) {
return false;
}
$this->BeforeDelete();
$tablename = $this->GetTableName();
if (Mysql::Query('DELETE FROM `' . $tablename . '` WHERE `' . $idstr . '`="' . Mysql::EscapeString($id) . '"')) {
if (isset(self::$objects[$tablename][$id]))
unset(self::$objects[$tablename][$id]);
$this->_deleted = true;
$this->_wasCreated = false;
$this->AfterDelete();
return true;
}
return false;
}
private $_wasCreated = false;
/**
* @return bool If the current in-memory object has been just inserted into database
*/
public function WasCreated() {
return $this->_wasCreated;
}
/**
* Inserts new record of object to database
* @param bool $force Whether to force the creation of new record or not (will insert new value, or clone existing object)
* @return int New id of object
*/
public function Create($force = false) {
if ($this->_deleted) {
trigger_error("Cannot create already deleted object: " . $this);
return false;
}
$idstr = $this->IdField();
$keys = array();
if (!$force) {
$keys = array_keys($this->_newdata);
} else {
$arr = $this->GetData();
if (isset($arr[$idstr]) && !$this->AllowIdSetting()) {
unset($arr[$idstr]);
}
$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 = $this->AllowIdSetting() ? $this->$idstr : 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->_wasCreated = true;
return $this->$idstr;
}
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] === '' || $arr[$fieldname] === null || !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 . '` ';
}
/**
* Object storage
* @var array
*/
private static $objects = array();
/**
* Gets object from object storage if possible, if not then requests from db
* @param BaseObject $obj Object, of which type objects are to be returned
* @param int $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();
}
//if object is cached, return it right away
if (isset(self::$objects[$tablename][$id]))
return self::$objects[$tablename][$id];
//not cached, load from db
$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[$tablename][$id] = $newobj;
}
return self::$objects[$tablename][$id];
}
/**
* Returns if object is cached currently or not
* @param BaseObject $obj
* @param int $id
* @return bool
*/
public static function HaveObjectCached(BaseObject $obj, $id = null) {
if (!isset(self::$objects[$obj->GetTableName()])) {
return false;
}
if ($id === null)
$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) {
$tablename = $obj->GetTableName();
$ret = count(self::$objects[$tablename]);
self::$objects[$tablename] = array();
return $ret;
}
$ret = 0;
foreach (self::$objects as $tableobjects)
$ret += count($tableobjects);
self::$objects = array();
return $ret;
}
/**
* Gets instance of this object from storage if possible
* @param int $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() {
$newobj = $this->GetNew();
$newobj->_data = array();
$newobj->_newdata = $this->GetData();
unset($newobj->_newdata[$this->IdField()]);
return $newobj;
}
/**
* Clears all data
*/
public function ClearData() {
$this->_data = array();
$this->_newdata = array();
}
/**
* @var array Table field description storage
*/
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) {
$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);
}
/**
* If we have internally cached db field info or not
* @return bool|null If DB info not loaded, returns null; loaded but no field in it, returns false; loaded and field is in it, returns true
*/
private function HasFieldInDBData($fieldname) {
$tablename = $this->GetTableName();
if (!isset(self::$allfields[$tablename]))
return null;
return array_key_exists($fieldname, self::$allfields[$tablename]);
}
/**
* 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 bool $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 (!$id) {
trigger_error('Invalid id provided, ignoring: ' . $id);
continue;
}
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 IDs 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
*/
private 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;
}
}
/**
* 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 results
* @param bool $collapse Whether to collapse 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);
}
}
|