<?php
/**
* @package DATA_MySQL5
*/
/**
* This class is the abstraction of a MySQL5 table row implementing the
* array access and iteration behavior.
*
* Field modification will affect the real database row, unless the object
* has been marked as read-only (example: when using integer index access
* to the field), or unless the object has been detached (example: cloned
* objects). Only the field modified will be affected.
*
* When cloning a row the new object will become detached from database and
* modifications won't be proxied to the database. The detached object will
* become reattached once it is inserted again the in table array. This can
* be used to bundle modifications to a row before commiting them, or
* duplicating rows with small changes.
*
* Data won't remain updated to external changes. Preferred use of row
* objects is as volatile containers.
*
* Field data returned will be inboxed into representative objects
* ({@link DATA_SQLChar}, {@link DATA_SQLVarchar}, {@link DATA_SQLDecimal},
* {@link DATA_SQLInt}, {@link DATA_SQLSmallInt}
* {@link DATA_SQLDate}, {@link DATA_SQLTime}, {@link DATA_SQLDatetime}).
*
* Examples:
* <code>
* // Retrieve a field value
* $fieldValue = $DB['table'][$rowOffset]['field'];
*
* // Change a field value
* $DB['table'][$rowOffset]['field'] = '...';
* // or
* $row = $DB['table'][$rowOffset];
* $row['field'] = '...';
*
* // Cloning
* $row = clone $DB['table'][$rowOffset];
* $row['field'] = '...';
* $row['field2'] = '...';
* $DB['table'][$rowOffset] = $row;
* // or on a new row or offset
* $DB['table'][] = $row;
* </code>
*/
class DATA_MySQL5_Row implements ArrayAccess, Countable, IteratorAggregate {
/**
* The table name.
* @var string
*/
protected $table;
/**
* The row data. It may be in raw or inboxed format.
* @var array
*/
protected $data;
/**
* Array for field checking purposes.
* @var array
*/
protected $fields;
/**
* Read only flag.
* @var bool
*/
protected $readOnly;
/**
* Detached from db flag
* @var bool
*/
protected $detached;
/**
* Disables inboxing in this object.
* @var bool
*/
protected $inboxingDisabled;
/**
* Default constructor.
*
* @access protected
* @param string $table The table.
* @param array $data The fetched row data.
* @param bool $readOnly The created object has read only access if set to true.
* Optional, defaults to false.
*/
public function __construct($table, $data, $readOnly = false) {
$this->table = $table;
$this->setData($data);
$this->readOnly = $readOnly;
$this->detached = false;
$this->inboxingDisabled = false;
}
/**
* isset(..) handler. Indicates if field exists.
*
* @param string $field The field name.
* @return bool True if field exists, false otherwise.
*/
public function offsetExists($field) {
if (is_int($field)) {
return count($this->data) > $field && $field >= 0;
} else {
return isset($this->fields[$field]);
}
}
/**
* [..] handler. Returns the value of the field requested.
*
* Throws {@link DATA_FieldDoesntExist}.
*
* @param string $field The field name.
* @return string The field value.
*/
public function offsetGet($field) {
if (!$this->offsetExists($field)) {
throw new DATA_FieldDoesntExist($this->table, $field);
}
if (is_int($field)) {
$fields = array_keys($this->data);
$field = $fields[$field];
}
return $this->inboxField($field, $this->data[$field]);
}
/**
* [..] = handler. Sets the value of a field.
*
* Throws {@link DATA_ReadOnly}, {@link DATA_FieldDoesntExist},
* {@link DATA_SQLTypeConstraintFailed}.
*
* @param string $field The field name.
* @param string $value The field value.
*/
public function offsetSet($field, $value) {
if ($this->readOnly) {
throw new DATA_ReadOnly();
}
if (!$this->offsetExists($field)) {
throw new DATA_FieldDoesntExist($this->table, $field);
}
if (is_int($field)) {
$fields = array_keys($this->data);
$field = $fields[$field];
}
try {
$value = $this->inboxField($field, $value);
} catch (DATA_SQLTypeConstraintFailed $exception) {
$exception->setTable($this->table);
$exception->setField($field);
throw $exception;
}
if (!$this->detached) {
$keys = DATA_MySQL5_Schema::getPrimaryKey($this->table);
DATA_MySQL5_Access::query("
UPDATE `{$this->table}`
SET `{$field}` = " . DATA_MySQL5_Access::prepareData($value) . "
WHERE `{$keys[0]}` = '" . DATA_MySQL5_Access::escape($this->data[$keys[0]]) . "'
");
}
$this->data[$field] = $value;
}
/**
* unset(..) handler. Fields cannot be unset.
*
* Will throw {@link DATA_CannotRemoveField DATA_CannotRemoveField exception}.
*/
public function offsetUnset($offset) {
throw new DATA_CannotRemoveField();
}
/**
* count(..) handler. Returns fields count.
*
* @return int How many fields there are on this row.
*/
public function count() {
return count($this->data);
}
/**
* Provides the iterator to be used on a foreach loop.
*
* @return ArrayIterator The fields data iterator.
*/
public function getIterator() {
if ($this->inboxingDisabled) {
return new ArrayIterator($this->data);
}
$inboxedData = array();
foreach ($this->data as $field => $value) {
$inboxedData[$field] = DATA_MySQL5_Schema::getSQLTypeFactory($this->table, $field)->inbox($value);
}
return new ArrayIterator($inboxedData);
}
/**
* clone handler. Detaches cloned object from the database.
*/
public function __clone() {
$this->readOnly = false;
$this->detached = true;
}
/**
* Reattaches the object to the database.
*
* @param int $autoid Autogenerated numeric id. Optional, 0 if no id was generated.
*/
public function reattach($autoid = 0) {
if ($autoid != 0) {
$autofield = DATA_MySQL5_Schema::getAutoIncrementField($this->table);
$this->data[$autofield] = (int)$autoid;
}
$sql = "SELECT * FROM `{$this->table}` ";
$sql .= $this->buildWhereStatement();
$query = DATA_MySQL5_Access::query($sql);
$this->setData(DATA_MySQL5_Access::fetchAssoc($query));
$this->readOnly = false;
$this->detached = false;
}
/**
* Builds a where statement to select this row.
*
* @return string SQL where statement to select the requested row.
*/
protected function buildWhereStatement() {
$keys = DATA_MySQL5_Schema::getPrimaryKey($this->table);
$sql = 'WHERE';
$sep = '';
foreach ($keys as $key) {
$sql .= "$sep `$key` = " . DATA_MySQL5_Access::prepareData($this->data[$key]);
$sep = ' AND';
}
return $sql;
}
/**
* Sets the fetched row data.
*
* @param array $data The fetched row data.
*/
protected function setData($data) {
$this->data = $data;
$this->fields = array();
foreach (array_keys($data) as $field) {
$this->fields[$field] = true;
}
}
/**
* Returns inboxed version of the field provided.
*
* Throws {@link DATA_SQLTypeConstraintFailed}.
*
* @param string $field The field name.
* @param null|int|string|DATA_SQLType $value The field value
* @return DATA_SQLType Inboxed field.
*/
protected function inboxField($field, $value) {
if ($this->inboxingDisabled) return $value;
return DATA_MySQL5_Schema::getSQLTypeFactory($this->table, $field)->inbox($value);
}
/**
* Member property overloading.
*
* withoutInboxing property returns a db object with inboxing of
* mysql types disabled.
*
* @param string $propname Property name.
* @return mixed Property value.
*/
public function __get($propname) {
if ($propname == 'withoutInboxing') {
$newRow = clone $this;
$newRow->readOnly = $this->readOnly;
$newRow->detached = $this->detached;
$newRow->inboxingDisabled = true;
return $newRow;
}
throw new Exception("Undefined property: {$propname}");
}
}
?>
|