Location: PHPKode > projects > Jethro Pastoral Ministry Manager > jethro-pmm/include/db_object.class.php
<?php

require_once 'MDB2/Date.php';
class db_object
{

	var $id = NULL;
	var $fields = Array();
	var $values = Array();

	var $_old_values = Array();
	var $_have_lock = NULL;
	var $_can_aquire_lock = NULL;

	var $_tmp = Array();


//--        CREATING, LOADING AND SAVING        --//

	function db_object($id=0)
	{
		$this->fields = $this->_getFields();
		foreach ($this->fields as $i => $v) {
			$this->fields[$i]['table_name'] = strtolower(get_class($this));
		}
		$parent_class = get_parent_class($this);
		while ($parent_class != 'db_object') {
			$new_fields = call_user_func(Array($parent_class, '_getFields'));
			foreach ($new_fields as $i => $v) {
				$new_fields[$i]['table_name'] = strtolower($parent_class);
			}
			$this->fields += $new_fields;
			$parent_class = get_parent_class($parent_class);
		}
		if ($id) {
			$this->load($id);
		} else {
			$this->loadDefaults();
		}
	}


	function getInitSQL()
	{
		$indexes = '';
		foreach ($this->_getIndexes() as $name => $fields) {
			$indexes .= ',
				UNIQUE KEY `'.$name.'` ('.implode(', ', $fields).')';
		}

		$res = "
			CREATE TABLE `".strtolower(get_class($this))."` (
			  `id` int(11) NOT NULL auto_increment,
				";
		foreach ($this->_getFields() as $name => $details) {
			$type = 'varchar(255)';
			$default = array_get($details, 'default', '');
			$null_exp = array_get($details, 'allow_empty', 0) ? 'NULL' : 'NOT NULL';
			switch ($details['type']) {
				case 'date':
					$type = 'date';
					break;
				case 'datetime':
					$type = 'datetime';
					break;
				case 'text':
					if (array_get($details, 'height', 1) != 1) {
						$type = 'text';
					} else {
						$type = 'varchar(255)';
					}
					break;
				case 'bibleref':
					$type = 'char(19)';
					break;
				case 'int':
					if (!is_null($len = array_get($details, 'fixed_length'))) {
						$type = 'varchar('.$len.')';
					} else {
						$type = 'int(11)';
					}
					$default = array_get($details, 'default', 0);
					break;
				case 'reference':
					$type = 'int(11)';
					$default = array_get($details, 'default', 0);
					break;
				case 'serialise':
					$type = 'text';
					break;
			}

			switch ($default) {
				case 'CURRENT_TIMESTAMP()':
				case 'NULL':
				case '0':
					break;
				default:
					$default = $GLOBALS['db']->quote($default);
					break;
			}

			$res .= "`".$name."` ".$type." ".$null_exp." default ".$default.",
				";
		}
		$res .= "PRIMARY KEY (`id`)".$indexes."
			) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;";
		return $res;
	}

	function _getIndexes()
	{
		return Array();
	}

	function create()
	{
		$GLOBALS['system']->setFriendlyErrors(TRUE);
		if (!$this->readyToCreate()) {
			return FALSE;
		}
		$GLOBALS['system']->setFriendlyErrors(FALSE);
		if (isset($this->fields['creator']) && empty($this->values['creator'])) {
			$userid = $GLOBALS['user_system']->getCurrentUser('id');
			if (!is_null($userid)) {
				$this->values['creator'] = $userid;
			}
		}
		if (isset($this->fields['history'])) {
			$this->values['history'] = Array(time() => 'Created');
		}

		$parent_class =  strtolower(get_parent_class($this));
		if ($parent_class != 'db_object') {
			$parent_obj =& new $parent_class();
			$parent_obj->populate(0, $this->values);
			if (!$parent_obj->create()) {
				return FALSE;
			}
			$this->id = $parent_obj->id;
		}

		return $this->_createFinal();
	}

	function _createFinal()
	{
		$db =& $GLOBALS['db'];
		$flds = Array();
		$vals = Array();
		$our_fields = $this->_getFields();
		foreach ($our_fields as $name => $details) {
			if (array_get($details, 'readonly')) continue;
			$flds[] = $name;
			$v = array_get($this->values, $name, '');
			if ($details['type'] == 'serialise') {
				$vals[] = $db->quote(serialize($v));
			} else {
				$vals[] = $db->quote($v);
			}
		}

		if ($this->id) {
			// if this class doesn't extend db_object directly then ID is not an auto-increment field
			// so we save it like the rest
			array_unshift($flds, 'id');
			array_unshift($vals, $db->quote((int)$this->id));
		}

		$sql = 'INSERT INTO '.strtolower(get_class($this)).' ('.implode(', ', $flds).')
				 VALUES ('.implode(', ', $vals).')';
		$res = $db->query($sql);
		check_db_result($res);
		$this->id = $db->lastInsertId();
		$this->_old_values = Array();
		return TRUE;
	}


	function createFromChild(&$child)
	{
		$this->populate($child->id, $child->values);
		return $this->_createFinal();
	}


	function _getTableNames()
	{
		$res = strtolower(get_class($this));
		$parent_class = strtolower(get_parent_class($this));
		while ($parent_class != 'db_object') {
			$res  = '('.$res.' JOIN '.$parent_class.' on '.$res.'.id = '.$parent_class.'.id)';
			$parent_class = strtolower(get_parent_class($parent_class));
		}
		return $res;
	}

	/**
	* Get the fields for this class only
	*
	* (Fields for parent classes are automatically added when instanting objects of this class
	*
	* @return array
	* @access protected
	*/
	function _getFields()
	{
		return Array();
	}


	function load($id)
	{
		$db =& $GLOBALS['db'];
		$sql = 'SELECT *
				FROM '.strtolower($this->_getTableNames()).'
				WHERE '.strtolower(get_class($this)).'.id = '.$db->quote($id);
		$res = $db->queryRow($sql);
		check_db_result($res);
		if (!empty($res)) {
			$this->id = $res['id'];
			unset($res['id']);
			$this->values = $res;
		}
		foreach ($this->fields as $name => $details) {
			if (($details['type'] == 'serialise') && isset($this->values[$name])) {
				$this->values[$name] = unserialize($this->values[$name]);
			}
		}
	}


	function loadDefaults()
	{
		foreach ($this->fields as $id => $details) {
			$this->values[$id] = array_get($details, 'default', '');
		}
	}


	function save()
	{
		$GLOBALS['system']->setFriendlyErrors(TRUE);
		if (!$this->validateFields()) {
			return FALSE;
		}
		$GLOBALS['system']->setFriendlyErrors(FALSE);

		if (!$this->haveLock() && !$this->acquireLock()) {
			trigger_error('Cannot save values because the lock has expired', E_USER_NOTICE);
			return FALSE;
		}

		if (empty($this->_old_values)) return TRUE;

		// Set the history
		if (isset($this->fields['history'])) {
			$changes = Array();
			foreach ($this->_old_values as $name => $old_val) {
				if ($name == 'history') continue;
				if ($name == 'password') continue;
				$changes[] = $this->getFieldLabel($name).' changed from "'.htmlentities($this->getFormattedValue($name, $old_val)).'" to "'.htmlentities($this->getFormattedValue($name)).'"';
			}
			$user = $GLOBALS['user_system']->getCurrentUser();
			$this->values['history'][time()] = 'Updated by '.$user['first_name'].' '.$user['last_name'].' (#'.$user['id'].")\n".implode("\n", $changes);
			$this->_old_values['history'] = 1;
		}

		// Set any last-changed fields
		foreach ($this->_old_values as $i => $v) {
			if (array_key_exists($i.'_last_changed', $this->fields)) {
				$this->values[$i.'_last_changed'] = date('c');
				$this->_old_values[$i.'_last_changed'] = 1;
			}
		}

		$parent_class = strtolower(get_parent_class($this));
		if ($parent_class != 'db_object') {
			$parent_obj =& new $parent_class($this->id);
			$parent_obj->populate($this->id, $this->values);
			if (!$parent_obj->save()) {
				return FALSE;
			}
		}
		
		// Update the DB
		$db =& $GLOBALS['db'];
		$sets = Array();
		$our_fields = $this->_getFields();
		foreach ($this->_old_values as $i => $v) {
			if (!isset($our_fields[$i])) continue;
			if (array_get($this->fields[$i], 'readonly')) continue;
			$new_val = $this->values[$i];
			if ($this->fields[$i]['type'] == 'serialise') {
				$new_val = serialize($new_val);
			}
			$sets[] = ''.$i.' = '.$db->quote($new_val).'';
		}
		if (!empty($sets)) {
			$sql = 'UPDATE '.strtolower(get_class($this)).'
					SET '.implode("\n, ", $sets).'
					WHERE id = '.$db->quote($this->id);
			$res = $db->query($sql);
			check_db_result($res);
		}

		$this->_old_values = Array();

		return TRUE;
	}

	function populate($id, $values)
	{
		$this->_old_values = Array();
		$this->id = $id;
		foreach ($this->fields as $fieldname => $details) {
			if (empty($details['readonly']) && isset($values[$fieldname])) {
				$this->setValue($fieldname, $values[$fieldname]);
			}
		}
		$this->_have_lock = NULL;
		$this->_can_acquire_lock = NULL;
	}

	function delete()
	{
		$db =& $GLOBALS['db'];
		$table_name = strtolower(get_class($this));
		while ($table_name != 'db_object') {
			$sql = 'DELETE FROM '.$table_name.' WHERE id='.$db->quote($this->id);
			$res = $db->query($sql);
			check_db_result($res);
			$table_name = strtolower(get_parent_class($this));
		}
	}





//--        GETTING AND SETTING FIELD VALUES        --//


	function setValue($name, $value)
	{
		if (!isset($this->fields[$name])) {
			trigger_error('Cannot set value for field '.htmlentities($name).' - field does not exist', E_USER_WARNING);
			return FALSE;
		}
		if (array_get($this->fields[$name], 'readonly')) {
			trigger_error('Cannot set value for readonly field "'.$name.'"', E_USER_WARNING);
			return;
		}
		if (array_get($this->fields[$name], 'initial_cap')) {
			$value = ucfirst($value);
		}
		if (array_get($this->fields[$name], 'trim')) {
			$value = trim($value, ",;. \t\n\r\0\x0B");
		}
		if ($this->fields[$name]['type'] == 'select') {
			if (!isset($this->fields[$name]['options'][$value])) {
				trigger_error(htmlentities($value).' is not a valid value for field "'.$name.'", and has not been set', E_USER_NOTICE);
				return;
			}
		}
		if (!empty($this->fields[$name]['maxlength']) && (strlen($value) > $this->fields[$name]['maxlength'])) {
			$value = substr($value, 0, $this->fields[$name]['maxlength']);
		}
		if ($this->fields[$name]['type'] == 'int') {
			if (!array_get($this->fields[$name], 'allow_empty', true) || ($value !== '')) {
				$strval = (string)$value;
				for ($i=0; $i < strlen($strval); $i++) {
					$char = $strval[$i];
					if ((int)$char != $char) {
						trigger_error(htmlentities($value).' is not a valid value for integer field "'.$name.'" and has not been set', E_USER_NOTICE);
						return;
					}
				}
			}
		}
		if (isset($this->values[$name]) && ($this->values[$name] != $value) && !isset($this->_old_values[$name])) {
			$this->_old_values[$name] = $this->values[$name];

		}
		$this->values[$name] = $value;
	}

	function getValue($name)
	{
		return array_get($this->values, $name);
	}



	function validateFields()
	{
		$res = TRUE;
		foreach ($this->fields as $id => $details) {
			$val = array_get($this->values, $id);
			if (!array_get($details, 'allow_empty', true) && (is_null($val) || ($val === ''))) {
				trigger_error($this->getFieldLabel($id).' is a required field for '.get_class($this).' and cannot be left empty', E_USER_NOTICE);
				$res = FALSE;
			}

			if (isset($details['max_length']) && (strlen($val) > $details['max_length'])) {
				trigger_error('The value for '.array_get($details, 'label', $id).' is too long (maximum is '.$details['max_length'].' characters)', E_USER_NOTICE);
				$res = FALSE;
			}

			if (isset($details['fixed_length']) && !empty($val) && (strlen($val) != $details['fixed_length'])) {
				trigger_error('The value for '.array_get($details, 'label', $id).' is not the correct length (must be exactly '.$details['fixed_length'].' characters)', E_USER_NOTICE);
				$res = FALSE;
			}
		}
		return $res;
	}


	function readyToCreate()
	{
		return $this->validateFields();

	}


//--        INTERFACE PAINTING AND PROCESSING        --//


	function printSummary()
	{
		?>
		<table class="standard">
		<?php
		$this->_printSummaryRows();
		?>
		</table>
		<?php
	}

	
	function _printSummaryRows()
	{
		foreach ($this->fields as $name => $details) {
			if (!array_get($details, 'show_in_summary', true)) continue;
			?>
			<tr>
				<th>
					<?php echo array_get($details, 'label', ucwords(str_replace('_', ' ', $name))); ?>
				</th>
				<td>
					<?php $this->printFieldValue($name); ?>
				</td>
			</tr>
			<?php
		}
	}

	// TODO: change this around to avoid ob_start
	function getFormattedValue($name, $value=null)
	{
		if (!isset($this->fields[$name])) {
			trigger_error('Cannot get value for field '.htmlentities($name).' - field does not exist', E_USER_WARNING);
			return NULL;
		}
		if (is_null($value)) $value = $this->values[$name];
		$field = $this->fields[$name];
		if (!empty($field['references'])) {
			$obj =& $GLOBALS['system']->getDBObject($field['references'], $value);
			if (!is_null($obj)) {
				if (!array_get($field, 'show_id', true)) {
					return $obj->toString();
				} else {
					return $obj->toString().' (#'.$value.')';
				}
			} else {
				if ($value != 0)  {
					return $value;
				}
			}
			return '';
		}
		if (($name == 'history') && !empty($value)) {
			ob_start();
			?>
			<table class="history standard">
			<?php
			foreach ($value as $time => $detail) {
				?>
				<tr>
					<th><?php echo format_datetime($time); ?></th>
					<td><?php echo nl2br(htmlentities($detail)); ?></td>
				</tr>
				<?php
			}
			?>
			</table>
			<?php
			return ob_get_clean();
		}
		switch ($field['type']) {
			case 'select':
				return array_get($field['options'], $value, '(Invalid Value)');
				break;
			case 'datetime':
				if (is_null($value) && array_get($field, 'allow_empty')) return '';
				return format_datetime($value);
				break;
			case 'bibleref':
				require_once 'bible_ref.class.php';
				$br = new bible_ref($value);
				return $br->toShortString();
				break;
			case 'text':
				if (array_get($field, 'height', 1) > 1) {
					return nl2br($value);
					break;
				}
				// deliberate fallthrough if text height is 1
			default:
				if (is_array($value)) {
					return '<pre>'.print_r($value, 1).'</pre>';
				} else {
					return $value;
				}
		}

	}


	function printFieldValue($name, $value=null)
	{
		echo $this->getFormattedValue($name, $value);
	}


	function printForm($prefix='', $fields=NULL)
	{
		?>
		<table>
		<?php
		foreach ($this->fields as $name => $details) {
			if (!is_null($fields) && !in_array($name, $fields)) continue;
			if (array_get($details, 'readonly')) continue;
			if (!array_get($details, 'editable', true)) continue;
			?>
			<tr>
				<th>
					<?php echo $this->getFieldLabel($name); ?>
				</th>
				<td>
					<?php 
					$this->printFieldInterface($name, $prefix); 
					if (!empty($this->fields[$name]['note'])) {
						echo '<div class="field-note">'.htmlentities($this->fields[$name]['note']).'</div>';
					}
					?>
				</td>
			</tr>
			<?php
		}
		?>
		</table>
		<?php

	}


	function getFieldLabel($id)
	{
		if (empty($id)) return;
		if (!isset($this->fields[$id])) {
			trigger_error('No such field '.$id);
			return;
		}
		return array_get($this->fields[$id], 'label', ucwords(str_replace('_', ' ', $id)));

	}

	function processForm($prefix='', $fields=NULL)
	{
		$GLOBALS['system']->setFriendlyErrors(TRUE);
		foreach ($this->fields as $name => $details) {
			if (!is_null($fields) && !in_array($name, $fields)) continue;
			if (array_get($details, 'readonly')) continue;
			if (!array_get($details, 'editable', true)) continue;
			$this->processFieldInterface($name, $prefix);
		}
		$GLOBALS['system']->setFriendlyErrors(FALSE);
	}

	function printFieldInterface($name, $prefix='')
	{
		$value = array_get($this->values, $name);
		if ($this->id && !$this->haveLock()) {
			echo $value;
		} else {
			print_widget($prefix.$name, $this->fields[$name], $value);
		}
	}

	function processFieldInterface($name, $prefix='')
	{
		if (!$this->id || $this->haveLock()) {
			$value = process_widget($prefix.$name, $this->fields[$name]);
			if (!is_null($value)) $this->setValue($name, $value);
		}

	}


//--        LOCKING        --//


	function haveLock()
	{
		if (!empty($GLOBALS['JETHRO_INSTALLING'])) return TRUE;
		//if (is_null($this->_have_lock)) {
			$db =& $GLOBALS['db'];
			$sql = 'SELECT COUNT(*)
					FROM  db_object_lock
					WHERE object_type = '.$db->quote(strtolower(get_class($this))).'
						AND objectid = '.$db->quote($this->id).'
						AND userid = '.$GLOBALS['user_system']->getCurrentUser('id').'
						AND expires > '.$db->quote(MDB2_Date::unix2Mdbstamp(time()));
			$this->_have_lock = $db->queryOne($sql);
			check_db_result($this->_have_lock);
		//}
		return $this->_have_lock;
	}

	function canAcquireLock()
	{
		if (!empty($GLOBALS['JETHRO_INSTALLING'])) return TRUE;
		if ($this->_have_lock) return TRUE; // easy!
		if (is_null($this->_can_aquire_lock)) {
			$db =& $GLOBALS['db'];
			$sql = 'SELECT userid
					FROM  db_object_lock
					WHERE object_type = '.$db->quote(strtolower(get_class($this))).'
						AND objectid = '.$db->quote($this->id).'
						AND expires > '.$db->quote(MDB2_Date::unix2Mdbstamp(time()));
			$res = $db->queryOne($sql);
			check_db_result($res);
			if ($res == $GLOBALS['user_system']->getCurrentUser('id')) {
				$this->_can_aquire_lock = TRUE; // already got it, what the heck
				$this->_have_lock = TRUE; // remember this for next time
			} else {
				$this->_can_aquire_lock = empty($res); // if nobody else has it, we can get it
			}
		}
		return $this->_can_aquire_lock;
	}

	function acquireLock()
	{
		if ($this->haveLock()) return TRUE;
		if (!$this->canAcquireLock()) return FALSE;
		$db =& $GLOBALS['db'];
		$sql = 'INSERT INTO db_object_lock (objectid, object_type, userid, expires)
				VALUES (
					'.$db->quote($this->id).',
					'.$db->quote(strtolower(get_class($this))).',
					'.$db->quote($GLOBALS['user_system']->getCurrentUser('id')).',
					'.$db->quote(MDB2_Date::unix2Mdbstamp(strtotime('+'.LOCK_LENGTH))).')';
		$res = $db->query($sql);
		check_db_result($res);
		$this->_have_lock = TRUE;

		if (rand(LOCK_CLEANUP_PROBABLILITY, 100) == 100) {
			$sql = 'DELETE FROM db_object_lock
					WHERE expires < '.$db->quote(MDB2_Date::unix2Mdbstamp(time()));
			$res = $db->query($sql);
			check_db_result($res);
		}

		return TRUE;
	}


	function releaseLock()
	{
		if (!$this->haveLock()) return;
		$db =& $GLOBALS['db'];
		$sql = 'DELETE FROM db_object_lock
				WHERE userid = '.$db->quote($GLOBALS['user_system']->getCurrentUser('id')).'
					AND objectid = '.$db->quote($this->id).'
					AND object_type = '.$db->quote(strtolower(get_class($this)));
		$res = $db->query($sql);
		check_db_result($res);
		$this->_have_lock = FALSE;
	}


//--        GLOBAL        --//


	function getInstancesQueryComps($params, $logic, $order)
	{
		$db =& $GLOBALS['db'];
		if ($logic != 'OR') $logic = 'AND';
		$res = Array();
		$res['select'] = Array(strtolower(get_class($this)).'.id');
		foreach ($this->fields as $fieldname => $details) {
			if ($details['type'] == 'serialise') continue;
			$fieldname = $details['table_name'].'.'.$fieldname;
			$res['select'][] = $fieldname;
		}
		$res['from'] = $this->_getTableNames();
		$wheres = Array();
		foreach ($params as $field => $val) {
			$operator = is_array($val) ? 'IN' : ((FALSE === strpos($val, '%')) && (FALSE === strpos($val, '?'))) ? '=' : 'LIKE';
				$prefix = '';
				$suffix = '';
			if ($field[0] == '!') {
				$prefix = 'NOT (';
				$field = substr($field, 1);
				$suffix = ')';
			} else if ($field[0] == '<') {
				$operator = '<';
				$field = substr($field, 1);
			} else if ($field[0] == '>') {
				$operator = '>';
				$field = substr($field, 1);
			} else if ($field[0] == '-') {
				$operator = 'BETWEEN';
				$field = substr($field, 1);
			} else if ($field[0] == '(') {
				$operator = 'IN';
				$field = substr($field, 1);
				$val = '('.$val.')';
			}
			$raw_field = $field;
			if ($field == 'id') {
				$field = strtolower(get_class($this)).'.'.$field;
			} else if (isset($this->fields[$field])) {
				$field = $this->fields[$field]['table_name'].'.'.$field;
			}
			if (isset($this->fields[$raw_field]) && $this->fields[$raw_field]['type'] == 'text') {
				$field = 'LOWER('.$field.')';
			}
			if ((is_array($val) && !empty($val))) {
				if ($operator == 'BETWEEN') {
					$field_details = array_get($this->fields, $field);
					if ($field_details && ($field_details['type'] == 'datetime') && (strlen($val[0]) == 10)) {
						// we're searching on a datetime field using date values
						// so extend them to prevent boundary errors
						$val[0] .= ' 00:00';
						$val[1] .= '23:59';
					}
					$wheres[] = '('.$field.' '.$operator.' '.$db->quote($val[0]).' AND '.$db->quote($val[1]).')';
				} else {
					$sub_wheres = Array();
					foreach ($val as $v) {
						$operator = ((FALSE === strpos($v, '%')) && (FALSE === strpos($v, '?'))) ? '=' : 'LIKE';
						if (isset($this->fields[$raw_field]) && $this->fields[$raw_field]['type'] == 'text') {
							$v = strtolower($v);
						}
						$sub_wheres[] = '('.$field.' '.$operator.' '.$db->quote($v).')';
					}
					$wheres[] = '('.$prefix.implode(' OR ', $sub_wheres).$suffix.')';
				}
			} else if ($operator == 'IN') {
				$wheres[] = '('.$prefix.$field.' '.$operator.' '.$val.$suffix.')';
			} else {
				if (isset($this->fields[$raw_field]) && $this->fields[$raw_field]['type'] == 'text') {
					$val = strtolower($val);
				}
				$wheres[] = '('.$prefix.$field.' '.$operator.' '.$db->quote($val).$suffix.')';
			}
		}

		$res['where'] = implode("\n\t".$logic.' ', $wheres);

		if (!empty($order)) {
			if (isset($this->fields[$order])) {
				$res['order_by'] = $this->fields[$order]['table_name'].'.'.$order;
			} else {
				$res['order_by'] = $order; // good luck...
			}
		}
		return $res;

	}

	function getInstancesData($params, $logic='OR', $order='')
	{
		$db =& $GLOBALS['db'];
		$query_bits = $this->getInstancesQueryComps($params, $logic, $order);
		$sql = 'SELECT '.implode(', ', $query_bits['select']).'
				FROM '.$query_bits['from'];
		if (!empty($query_bits['where'])) {
				$sql .= '
					WHERE '.$query_bits['where'];
		}
		if (!empty($query_bits['group_by'])) {
			$sql .= '
					GROUP BY '.$query_bits['group_by'];
		}
		if (!empty($query_bits['order_by'])) {
			$sql .= '
					ORDER BY '.$query_bits['order_by'];
		}
		$res = $db->queryAll($sql, null, null, true);
		check_db_result($res);
		return $res;

	}//end getInstances()

	function toString()
	{
		if (array_key_exists('name', $this->fields)) {
			return $this->getValue('name');
		} else if (array_key_exists('title', $this->fields)) {
			return $this->getvalue('title');
		} else {
			return get_class($this).' #'.$this->id;
		}
	}


}//end class
Return current item: Jethro Pastoral Ministry Manager