array(TYPE, REQUIRED, VERIFY METHOD (:self for self-named method), RELATION => array(FILE, CLASS IN FILE, ALTERNATE FIELD NAME)) * @var array */ protected $fields = array(); /** * Values array: sanitized and verified field values * @var array */ public $values = array(); /** * Fields that were manually set with set(), not by using setExisting() * @var array */ private $setfields = array(); /** * WHERE condition * @var string */ private $condition = ''; /** * The object table row; a fetched row that represents this instance * @var array */ public $objdata = array(); /** * Insert ID from the insert() command * @var integer */ public $insertid = 0; /** * Error queue that builds up errors * @var array */ private $errors = array(); // ################################################################### /** * Constructor: cannot instantiate class directly */ public function __construct() { BSApp::RequiredModules(array('Db', 'Input')); } // ################################################################### /** * Adds an error into the error queue that is then hit * * @param string Error message */ protected function _error($message) { $this->errors[] = $message; } // ################################################################### /** * This runs through all the errors in the error queue and processes * them all at once. Error builders then get a list of all the errors, * and die-by-hit callbacks stop at the first one * * @param string A string param * * @return integer Return value */ private function _processErrorQueue() { // we want to explicitly specify silence if (self::$errorHandler == 'silent') { return; } if (!is_callable(self::$errorHandler)) { throw new Exception('No BSApi::$errorHandler handler has been set'); } foreach ($this->errors AS $e) { call_user_func(BSApi::$errorHandler, $e); } } // ################################################################### /** * Returns the error list. This is because we don't want people mucking * with the error system. It will return an empty array if there are * no errors. * * @return array Array of errors */ public function checkErrors() { if (sizeof($this->errors) < 1) { return array(); } return $this->errors; } // ################################################################### /** * Sets a value, sanitizes it, and verifies it * * @param string Field name * @param mixed Value * @param bool Do clean? * @param bool Do verify? */ public function set($field, $value, $doclean = true, $doverify = true) { if (!isset($this->fields["$field"])) { throw new Exception('Field "' . $field . '" is not valid'); return; } $this->values["$field"] = ($doclean ? BSApp::GetType('Input')->clean($value, $this->fields["$field"][F_TYPE]) : $value); $this->setfields["$field"] = $field; if (isset($this->fields["$field"][F_VERIFY]) AND $doverify) { if ($this->fields["$field"][F_VERIFY] == ':self') { $verify = $this->{"verify_$field"}($field); } else { $verify = $this->{$this->fields["$field"][F_VERIFY]}($field); } if ($verify !== true) { if ($verify === false) { $this->_error(sprintf(_('Validation of "%1$s" failed'), $field)); } else { $this->_error($verify); } } } } // ################################################################### /** * Sets the condition to use in the WHERE clause; if not passed, then * it calculates it from the REQ_AUTO field * * @param mixed String with WHERE condition; array of fields to use for WHERE builder */ public function setCondition($condition = '') { if (is_array($condition) AND sizeof($condition) > 0) { $this->condition = ''; foreach ($condition AS $field) { if (!$this->values["$field"]) { throw new Exception('The specified field "' . $field . '" for the condition could not be found as it is not set'); continue; } $condbits[] = "$field = " . $this->_prepareFieldForSql($field); } $this->condition = implode(' AND ', $condbits); } else if ($condition != '') { $this->condition = $condition; } else { foreach ($this->fields AS $name => $options) { if ($options[F_REQ] == REQ_AUTO) { if (!$this->values["$name"]) { throw new Exception('Cannot determine condition from the REQ_AUTO field because it is not set'); continue; } $this->condition = "$name = " . $this->_prepareFieldForSql($name); } } if ($this->condition == '') { throw new Exception('No REQ_AUTO fields are present and therefore the condition cannot be created'); } } } // ################################################################### /** * Sets existing data into $values where it's not already present */ public function setExisting() { static $run; if ($run) { return; } $this->fetch(); foreach ($this->objdata AS $field => $value) { if (!isset($this->values["$field"])) { $this->values["$field"] = $value; } } $run = true; } // ################################################################### /** * Fetches a record based on the condition * * @param bool Run pre_fetch()? * @param bool Run post_fetch()? * * @return boolean Whether or not the row was successfully fetched */ public function fetch($doPre = true, $doPost = true) { if ($this->condition == '') { $this->setCondition(); } // reset the error queue due to any validation errors caused by fetchable fields $this->errors = array(); $this->_runActionMethod('pre_fetch', $doPre); $result = BSApp::GetType('Db')->queryFirst("SELECT * FROM {$this->prefix}{$this->table} WHERE {$this->condition}"); if (!$result) { return false; } $this->_runActionMethod('post_fetch', $doPost); $this->objdata = $result; return true; } // ################################################################### /** * Inserts a record in the database * * @param bool Run pre_insert()? * @param bool Run post_insert()? */ public function insert($doPre = true, $doPost = true) { $this->verify(); $this->_processErrorQueue(); $this->_runActionMethod('pre_insert', $doPre); foreach ($this->setfields AS $field) { $fields[] = $field; $values[] = $this->_prepareFieldForSql($field); } BSApp::GetType('Db')->query("INSERT INTO {$this->prefix}{$this->table} (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ")"); if (BSApp::GetType('DbPostgreSql')) { foreach ($this->fields AS $field => $info) { if ($info[F_REQ] == REQ_AUTO) { $autofield = $field; break; } } $this->insertid = BSApp::GetType('Db')->insertId($this->prefix . $this->table, $autofield); } else { $this->insertid = BSApp::GetType('Db')->insertId(); } $this->_runActionMethod('post_insert', $doPost); } // ################################################################### /** * Updates a record in the database using the data in $vaues * * @param bool Run pre_update()? * @param bool Run post_update()? */ public function update($doPre = true, $doPost = true) { if ($this->condition == '') { $this->setCondition(); } $this->_processErrorQueue(); $this->_runActionMethod('pre_update', $doPre); foreach ($this->setfields AS $field) { $updates[] = "$field = " . $this->_prepareFieldForSql($field); } $updates = implode(', ', $updates); BSApp::GetType('Db')->query("UPDATE {$this->prefix}{$this->table} SET $updates WHERE {$this->condition}"); $this->_runActionMethod('post_update', $doPost); } // ################################################################### /** * Deletes a record * * @param bool Run pre_remove()? * @param bool Run post_remove()? */ public function remove($doPre = true, $doPost = true) { if ($this->condition == '') { $this->setCondition(); } $this->fetch(); $this->_runActionMethod('pre_remove', $doPre); BSApp::GetType('Db')->query("DELETE FROM {$this->prefix}{$this->table} WHERE {$this->condition}"); $this->_runActionMethod('post_remove', $doPost); } // ################################################################### /** * Verifies that all required fields are set */ private function verify() { foreach ($this->fields AS $name => $options) { if ($options[F_REQ] == REQ_YES) { if (!isset($this->values["$name"])) { $this->_error(sprintf(_('The required field "%1$s" was not set'), $name)); } } else if ($options[F_REQ] == REQ_SET) { $this->{"set_$name"}(); } } } // ################################################################### /** * Runs a pre- or post-action method for database commands * * @param string Action to run * @param bool Actually run it? */ private function _runActionMethod($method, $doRun) { if (!$doRun) { return; } $actmethod = (method_exists($this, $method) ? $this->$method() : ''); } // ################################################################### /** * Prepares a value for use in a SQL query; it encases and escapes * strings and string-like values * * @param string Field name * * @return string Prepared value entry */ private function _prepareFieldForSql($name) { $type = $this->fields["$name"][F_TYPE]; if ($type == TYPE_NONE OR $type == TYPE_STR OR $type == TYPE_STRUN) { return "'" . BSApp::GetType('Db')->escapeString($this->values["$name"]) . "'"; } else if ($type == TYPE_BOOL) { return ($this->values["$name"] == true ? "'1'" : "'0'"); } else if ($type == TYPE_BIN) { return "'" . BSApp::GetType('Db')->escapeBinary($this->values["$name"]) . "'"; } else { return $this->values["$name"]; } } // ################################################################### /** * Verify field: not a zero value */ protected function verify_nozero($field) { if ($this->values["$field"] == 0) { return sprintf(_('The field "%1$s" cannot be zero'), $field); } return true; } // ################################################################### /** * Verify field: not empty */ protected function verify_noempty($field) { if (empty($this->values["$field"])) { return sprintf(_('The field "%1$s" cannot be empty'), $field); } return true; } // ################################################################### /** * Verify field: unique */ protected function verify_unique($field) { $res = BSApp::GetType('Db')->queryFirst("SELECT $field FROM {$this->prefix}{$this->table} WHERE $field = " . $this->_prepareFieldForSql($field) . (empty($this->condition) ? "" : " AND !({$this->condition})")); if ($res) { return sprintf(_('The "%1$s" field must contain a unique value'), $field); } return true; } } ?>