set('foo', 'abc'); * 3. $o->set('test', 45); * 4. try { $o->insert(); } catch (ApiException $e) {} * * @author Blue Static * @copyright Copyright (c)2005 - 2009, Blue Static * @package ISSO * */ abstract class BSApi { /** * Fields: used for verification and sanitization * NAME => array(TYPE, REQUIRED) * @var array */ protected $fields = array(); /** * The table name the API maps objects for * @var string */ protected $table = '___UNDEFINED___'; /** * The database table prefix * @var string */ protected $prefix = ''; /** * Values array: sanitized and validated field values * @var array */ public $values = array(); /** * Fields that were set by the client * @var array */ private $setfields = array(); /** * WHERE condition * @var string */ protected $condition = ''; /** * The object table row; a fetched row that represents this instance * @var array */ public $record = array(); /** * Insert ID from the insert() command * @var integer */ public $insertid = 0; /** * Error queue that builds up errors * @var ApiException */ private $exception = null; /** * Constructor */ public function __construct() { if (!BSApp::$input instanceof BSInput) { throw new Exception('BSApp::$input is not an instance of BSInput'); } if (!BSApp::$db instanceof BSDb) { throw new Exception('BSApp::$db is not an instance of BSDb'); } } /** * Adds an error into the error queue that is then hit * * @param Exception Error message */ protected function _error(Exception $e) { if ($this->exception == null) { $this->exception = new ApiException(); } $this->exception->addException($e); } /** * This simply throws the ApiException if it exists, which inside holds * all of the individual and specific errors */ protected function _processErrorQueue() { if ($this->exception) { throw $this->exception; } } /** * Sets an array of data into the API, ignoring things in $exclude. Keys * in the array that don't exist in the API will be ignored. * * @param array A dictionary of field names and values to set * @param array Array of keys to exclude */ public function setArray(Array $data, $exclude = array()) { foreach ($data as $key => $value) { if (in_array($key, $exclude) || !isset($this->fields[$key])) { continue; } $this->set($key, $value); } } /** * Resets the API object to an initial state. This will NOT clear the primary (REQ_AUTO) * field. */ public function reset() { foreach ($this->fields as $field => $settings) { if ($settings[F_REQ] == REQ_AUTO) { $savename = $field; $savevalue = $this->fetchValue($field); $savevalue = ($savevalue ? $savevalue : $this->insertid); break; } } $this->setfields = array(); $this->values = array(); $this->condition = ''; $this->insertid = 0; $this->exception = null; $this->set($savename, $savevalue); } /** * Sets a value, sanitizes it, and validates it * * @param string Field name * @param mixed Value * @param bool Do clean? * @param bool Do validation? */ public function set($field, $value, $doclean = true, $dovalidate = true) { if (!isset($this->fields["$field"])) { throw new Exception('Field "' . $field . '" is not valid'); } $this->values["$field"] = ($doclean ? BSApp::$input->clean($value, $this->fields["$field"][F_TYPE]) : $value); $this->setfields["$field"] = $field; if ($dovalidate && method_exists($this, "validate_$field")) { $this->{"validate_$field"}($field); } } /** * 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 = null) { if (is_array($condition) && 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'); } $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'); } $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'); } } } /** * 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 = null; $this->_runActionMethod('pre_fetch', $doPre); $result = BSApp::$db->queryFirst("SELECT * FROM {$this->prefix}{$this->table} WHERE {$this->condition}"); if (!$result) { return false; } $this->_runActionMethod('post_fetch', $doPost); $this->record = $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->_verifyRequired(); $this->_processErrorQueue(); $this->_runActionMethod('pre_insert', $doPre); $fields = $values = array(); foreach ($this->setfields as $field) { $fields[] = $field; $values[] = $this->_prepareFieldForSql($field); } BSApp::$db->query("INSERT INTO {$this->prefix}{$this->table} (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ")"); if (BSApp::$db instanceof BSDbPostgreSql) { foreach ($this->fields as $field => $info) { if ($info[F_REQ] == REQ_AUTO) { $autofield = $field; break; } } $this->insertid = BSApp::$db->insertId($this->prefix . $this->table, $autofield); } else { $this->insertid = BSApp::$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::$db->query("UPDATE {$this->prefix}{$this->table} SET $updates WHERE {$this->condition}"); $this->_runActionMethod('post_update', $doPost); } /** * Deletes a record * * @param bool Run pre_delete()? * @param bool Run post_delete()? */ public function delete($doPre = true, $doPost = true) { if (!$this->condition) { $this->setCondition(); } $this->fetch(); $this->_runActionMethod('pre_delete', $doPre); BSApp::$db->query("DELETE FROM {$this->prefix}{$this->table} WHERE {$this->condition}"); $this->_runActionMethod('post_delete', $doPost); } /** * Verifies that all required fields are set */ protected function _verifyRequired() { foreach ($this->fields as $name => $options) { if ($options[F_REQ] == REQ_YES) { if (!isset($this->values["$name"])) { $this->_error(new FieldException(sprintf(_('The required field "%1$s" was not set'), $name), $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? */ protected function _runActionMethod($method, $doRun) { if (!$doRun || !method_exists($this, $method)) { return; } $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 */ protected function _prepareFieldForSql($name) { $type = $this->fields["$name"][F_TYPE]; if ($type == TYPE_NONE || $type == TYPE_STR || $type == TYPE_STRUN) { return "'" . BSApp::$db->escapeString($this->values["$name"]) . "'"; } else if ($type == TYPE_BOOL) { return ($this->values["$name"] == true ? "'1'" : "'0'"); } else if ($type == TYPE_BIN) { return "'" . BSApp::$db->escapeBinary($this->values["$name"]) . "'"; } else { return $this->values["$name"]; } } /** * Determines the value of a field from Api->record[], Api->values[] (in that order) * * @param string The field ID to determine for * * @return mixed */ public function fetchValue($field) { if ($this->record[$field]) { return $this->record[$field]; } else if ($this->values[$field]) { return $this->values[$field]; } return null; } /** * Verify field: not a zero value */ protected function _verifyIsNotZero($field) { if ($this->values[$field] == 0) { $this->_error(new FieldException(sprintf(_('The field "%1$s" cannot be zero'), $field), $field)); return false; } return true; } /** * Verify field: not empty */ protected function _verifyIsNotEmpty($field) { if (empty($this->values[$field])) { $this->_error(new FieldException(sprintf(_('The field "%1$s" cannot be empty'), $field), $field)); return false; } return true; } /** * Verify field: unique */ protected function _verifyIsNotUnique($field) { $res = BSApp::$db->queryFirst("SELECT $field FROM {$this->prefix}{$this->table} WHERE $field = " . $this->_prepareFieldForSql($field) . (empty($this->condition) ? "" : " AND !({$this->condition})")); if ($res) { $this->_error(new FieldException(sprintf(_('The "%1$s" field must contain a unique value'), $field), $field)); return false; } return true; } } /** * API Exception * * This class is an exception container that can be used to store a series * of exceptions that can be thrown as one * * @author rsesek * @copyright Copyright (c)2005 - 2009, Blue Static * @package ISSO * */ class ApiException extends Exception { /** * Array of exceptions * @var array */ private $exceptions = array(); /** * Constructor */ public function __construct() { parent::__construct(_('An error occurred while processing the API data. Errors: ')); } /** * Adds an exception to the collection * * @param Exception $e */ public function addException(Exception $e) { $this->exceptions[] = $e; $this->message .= ' (' . sizeof($this->exceptions) . ') ' . $e->getMessage(); } /** * Returns an array of all the exceptions in the collection * * @return array */ public function getExceptions() { return $this->exceptions; } } /** * Field Exception * * This exception represents a problem with an API field * * @author rsesek * @copyright Copyright (c)2005 - 2009, Blue Static * @package ISSO * */ class FieldException extends Exception { /** * The name of the erroring field * @var string */ private $field; /** * Constructor: create a new exception */ public function __construct($error, $field) { $this->field = $field; parent::__construct($error); } /** * Returns the name of the field the exception is for * * @return string */ public function getField() { return $this->field; } } ?>