<?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}");
 
    }
 
}
 
?>
 
 
 |