<?php
/*
* $Id: Record.php,v 1.10 2005/07/13 17:28:13 hlellelid Exp $
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information please see
* <http://creole.phpdb.org>.
*
* This product includes software based on the Village framework,
* http://share.whichever.com/index.php?SCREEN=village.
*/
require_once 'creole/CreoleTypes.php';
// These classes must be included so the "instanceof" calls
// below will be able to function properly.
include_once 'jargon/QueryDataSet.php';
/**
* A Record represents a row in the database. It contains a hash of
* values which represent the column values for each row.
*
* @author Jon S. Stevens <hide@address.com> (Village)
* @author Hans Lellelid <hide@address.com> (Jargon)
* @version $Revision: 1.10 $
* @package jargon
*/
class Record {
// saveType constants
const ZOMBIE = -1;
const UNKNOWN = 0;
const INSERT = 1;
const UPDATE = 2;
const DELETE = 3;
const BEFOREINSERT = 4;
const AFTERINSERT = 5;
const BEFOREUPDATE = 6;
const AFTERUPDATE = 7;
const BEFOREDELETE = 8;
const AFTERDELETE = 9;
/** an array of values strings, indexed by column name.*/
private $values = array();
/** array of modified (dirty) columns */
private $dirtyCols = array();
/** the parent DataSet for this Record */
private $ds;
/** this is the state of this record */
private $saveType = 0;
/**
* Creates a new Record and sets the parent dataset to the passed in value.
*
* If $addRecord is true, then an empty record is created.
*
* @param DataSet $ds The parent / owning dataset.
* @param boolean $addRecord Whether to create an empty record.
*/
function __construct(DataSet $ds, $addRecord = false)
{
$this->setParentDataSet($ds);
if (!$addRecord) {
$this->createValues($this->ds->resultSet());
}
}
/**
* Performs initialization for this Record.
*/
private function initializeRecord()
{
$this->values = array();
$this->dirtyCols = array();
$this->setSaveType(Record::UNKNOWN);
}
/**
* Creates the value objects for this Record. It is 1 based
*
* @return void
*/
private function createValues(ResultSet $rs)
{
$this->values = $rs->getRow();
}
/**
* Shortcut method to delete this record.
* @param Connection $conn
*/
public function delete(Connection $conn = null)
{
$this->setSaveType(DELETE);
$this->save($conn);
}
/**
* Saves the data in this Record to the database.
* @param Connection $conn
* @return boolean True if the save completed. false otherwise.
* @throws DataSetException
*/
public function save(Connection $conn = null)
{
$returnValue = false;
if ($this->ds instanceof QueryDataSet) {
throw new DataSetException("You cannot save a QueryDataSet. Please use a TableDataSet instead.");
}
if (!$this->needsToBeSaved()) {
return $returnValue;
}
switch($this->saveType) {
case Record::INSERT:
$returnValue = $this->doInsert($conn);
break;
case Record::UPDATE:
$returnValue = $this->doUpdate($conn);
break;
case Record::DELETE:
$returnValue = $this->doDelete($conn);
break;
default:
throw new DataSetException("Invalid or no-action saveType for Record.");
}
return (boolean) $returnValue;
}
/**
* Performs a DELETE on databse using this Record as criteria.
* @return int Number of rows affected by delete.
* @throws DataSetException, SQLException
*/
private function doDelete(Connection $conn = null)
{
if ($conn === null) {
$conn = $this->ds->connection();
}
$table = $this->ds->tableInfo();
$stmt = null;
try {
$stmt = $conn->prepareStatement($this->getDeleteSql());
$ps = 1;
$kd = $this->ds->keydef();
for ($i = 1, $kdsize = $kd->size(); $i <= $kdsize; $i++) {
$col = $kd->getAttrib($i);
$val = $this->getValue($col);
$setter = 'set' . CreoleTypes::getAffix( $table->getColumn($col)->getType() );
$stmt->$setter($ps++, $val);
}
$ret = $stmt->executeUpdate();
// note that the actual removal of the Record objects
// from the DataSet is done by the TDS::save() method.
$this->setSaveType(Record::ZOMBIE);
$stmt->close();
if ($ret > 1) {
throw new SQLException("There were " . $ret . " rows deleted with this records key value.");
}
return $ret;
} catch (SQLException $e) {
if ($stmt) $stmt->close();
throw $e;
}
}
/**
* Saves the data in this Record to the database with an UPDATE statement.
* @return SQL UPDATE statement
* @throws DataSetException, SQLException
*/
private function doUpdate(Connection $conn = null)
{
if ($conn === null) {
$conn = $this->ds->connection();
}
$table = $this->ds->tableInfo();
$stmt = null;
try {
$stmt = $conn->prepareStatement($this->getUpdateSql());
$ps = 1;
foreach($this->dirtyColumns() as $col) {
$setter = 'set' . CreoleTypes::getAffix( $table->getColumn($col)->getType() );
$stmt->$setter($ps++, $this->getValue($col));
}
$kd = $this->ds->keydef();
for ($i = 1, $kdsize = $kd->size(); $i <= $kdsize; $i++) {
$attrib = $kd->getAttrib($i);
$setter = 'set' . CreoleTypes::getAffix( $table->getColumn($attrib)->getType() );
$stmt->$setter($ps++, $this->getValue($attrib));
}
$ret = $stmt->executeUpdate();
if ($this->ds->refreshOnSave()) {
$this->refresh();
} else {
// Marks all of the values clean since they have now been saved
$this->markRecordClean();
}
$this->setSaveType(Record::AFTERUPDATE);
if ($ret > 1) {
throw new SQLException ("There were " . $ret . " rows updated with this records key value.");
}
return $ret;
} catch (SQLException $e) {
if ($stmt) $stmt->close();
throw $e;
}
}
/**
* Saves the data in this Record to the database with an INSERT statement
* @return int
* @throws DataSetException, SQLException
*/
private function doInsert(Connection $conn = null)
{
$stmt = null;
try {
$stmt = $conn->prepareStatement($this->getInsertSql());
$ps = 1;
foreach($this->dirtyColumns() as $col) {
$val = $this->getValue($col);
$setter = 'set' . CreoleTypes::getAffix( $table->getColumn($col)->getType() );
$stmt->$setter($ps++, $val);
}
$ret = $stmt->executeUpdate();
if ($this->ds->refreshOnSave()) {
$this->refresh();
} else {
// Marks all of the values clean since they have now been saved
$this->markRecordClean();
}
$this->setSaveType(Record::AFTERINSERT);
if ($ret > 1) {
// a little late again...
throw new SQLException ("There were " . $ret . " rows inserted with this records key value.");
}
return $ret;
} catch (SQLException $e) {
if ($stmt) $stmt->close();
throw $e;
}
}
/**
* Builds the SQL UPDATE statement for this Record
* @return string SQL UPDATE statement
* @throws DataSetException
*/
private function getUpdateSql()
{
$kd = $this->ds->keydef();
if ($kd === null || $kd->size() === 0) {
throw new DataSetException("You must specify KeyDef attributes for this TableDataSet in order to create a Record for update.");
} elseif ($this->recordIsClean()) {
throw new DataSetException ("You must Record->setValue() on a column before doing an update.");
}
$set_sql = "";
$where_sql = "";
$comma = false;
foreach($this->dirtyColumns() as $col) {
if (!$comma) {
$set_sql .= $col . " = ?";
$comma = true;
} else {
$set_sql .= ", " . $col . " = ?";
}
}
$comma = false;
for ($i = 1, $kdsize = $kd->size(); $i <= $kdsize; $i++) {
$attrib = $kd->getAttrib($i);
if (! $this->valueIsClean ($attrib)) {
throw new DataSetException ("The value for column '" . $attrib . "' is a key value and cannot be updated.");
}
if (!$comma) {
$where_sql .= $attrib . " = ?";
$comma = true;
} else {
$where_sql .= " AND " . $attrib . " = ?";
}
}
return "UPDATE " . $this->ds->tableName() . " SET " . $set_sql . " WHERE " . $where_sql;
}
/**
* Builds the SQL DELETE statement for this Record.
* @return string SQL DELETE statement
* @throws DataSetException - if no keydef
*/
private function getDeleteSql()
{
$kd = $this->ds->keydef();
if ($kd === null || $kd->size() === 0) {
throw new DataSetException("You must specify KeyDef attributes for this TableDataSet in order to delete a Record.");
}
$where_sql = "";
$comma = false;
for ($i = 1, $kdsize = $kd->size(); $i <= $kdsize; $i++) {
if (!$comma) {
$where_sql .= $kd->getAttrib($i) . " = ?";
$comma = true;
} else {
$where_sql .= " AND " . $kd->getAttrib($i) . " = ?";
}
}
return "DELETE FROM " . $this->ds->tableName() . " WHERE " . $where_sql;
}
/**
* Builds the SQL INSERT statement for this Record
* @return string SQL INSERT statement
*/
private function getInsertSql()
{
$fields_sql = "";
$values_sql = "";
$comma = false;
foreach($this->dirtyColumns() as $col) {
if (!$comma) {
$fields_sql .= $col;
$values_sql .= "?";
$comma = true;
} else {
$fields_sql .= ", " . $col;
$values_sql .= ", ?";
}
}
return "INSERT INTO " . $this->ds->tableName() . " ( " . $fields_sql . " ) VALUES ( " . $values_sql . " )";
}
/**
* Gets the value for specified column.
* This function performs no type-conversion.
* @return string The value object for specified column as string.
* @throws DataSetException
*/
public function getValue($col)
{
if (!isset($this->values[$col])) {
throw new DataSetException("Undefined column in Record: " . $col);
}
return $this->values[$col];
}
/**
* Get the column names for current record.
* @return array Column names.
*/
public function columns()
{
return array_keys($this->values);
}
/**
* Get the modified (dirty) columns.
* Private right now because this is only used internally. No
* real reason why this couldn't be public, though ...
* @return array
*/
private function dirtyColumns()
{
return array_keys($this->dirtyCols);
}
/**
* The number of columns in this object.
* @return the number of columns in this object
*/
public function size()
{
return count($this->values);
}
/**
* Whether or not this Record is to be saved with an SQL insert statement
* @return boolean True if saved with insert
*/
public function toBeSavedWithInsert()
{
return ($this->saveType === Record::INSERT);
}
/**
* Whether or not this Record is to be saved with an SQL update statement
* @return boolean True if saved with update
*/
public function toBeSavedWithUpdate()
{
return ($this->saveType === Record::UPDATE);
}
/**
* Whether or not this Record is to be saved with an SQL delete statement
* @return boolean True if saved with delete
*/
public function toBeSavedWithDelete()
{
return ($this->saveType === Record::DELETE);
}
/**
* Marks all the values in this record as clean.
* @return void
*/
public function markRecordClean()
{
$this->dirtyCols = array();
}
/**
* Marks this record to be inserted when a save is executed.
* @return void
* @throws DataSetException - if DataSet is not TableDataSet
*/
public function markForInsert()
{
if ($this->ds instanceof QueryDataSet) {
throw new DataSetException ("You cannot mark a record in a QueryDataSet for insert");
}
$this->setSaveType(Record::INSERT);
}
/**
* Marks this record to be updated when a save is executed.
* @return void
* @throws DataSetException - if DataSet is not TableDataSet
*/
public function markForUpdate()
{
if ($this->ds instanceof QueryDataSet) {
throw new DataSetException ("You cannot mark a record in a QueryDataSet for update");
}
$this->setSaveType(Record::UPDATE);
}
/**
* Marks this record to be deleted when a save is executed.
* @return void
* @throws DataSetException - if DataSet is not TableDataSet
*/
public function markToBeDeleted()
{
if ($this->ds instanceof QueryDataSet) {
throw new DataSetException ("You cannot mark a record in a QueryDataSet for deletion");
}
$this->setSaveType(Record::DELETE);
// return this;
}
/**
* Unmarks a record that has been marked for deletion.
* <P>
* WARNING: You must reset the save type before trying to save this record again.
*
* @see markForUpdate()
* @see markForInsert()
* @see markToBeDeleted()
* @throws DataSetException
*/
public function unmarkToBeDeleted()
{
if ($this->saveType === Record::ZOMBIE) {
throw new DataSetException ("This record has already been deleted!");
}
$this->setSaveType(UNKNOWN);
//return $this;
}
/**
* Marks a value with a given column name as clean (unmodified).
* @param string $col
* @return void
*/
public function markValueClean($col)
{
unset($this->dirtyCols[$col]);
}
/**
* Marks a value with a given column as "dirty" (modified).
* @param string $col
* @return void
*/
public function markValueDirty($col)
{
$this->dirtyCols[$col] = true;
}
/**
* Sets the internal save type as one of the defined privates (ie: ZOMBIE)
* @param int $type
* @return void
*/
public function setSaveType($type)
{
$this->saveType = $type;
}
/**
* Gets the internal save type as one of the defined privates (ie: ZOMBIE)
* @return int
*/
public function getSaveType()
{
return $this->saveType;
}
/**
* Sets the value of col.
* @return Record this object.
* @throws DataSetException
*/
public function setValue ($col, $value)
{
$this->values[$col] = $value;
$this->markValueDirty($col);
return $this;
}
/**
* Determines if this record is a Zombie. A Zombie is a record that has been deleted from the
* database, but not yet removed from the DataSet.
*
* @return boolean
*/
public function isAZombie()
{
return ($this->saveType === Record::ZOMBIE);
}
/**
* If the record is not clean, needs to be saved with an Update, Delete or Insert, it returns true.
* @return boolean
*/
public function needsToBeSaved()
{
return (!$this->isAZombie() || !$this->recordIsClean() || $this->toBeSavedWithUpdate() ||
$this->toBeSavedWithDelete() || $this->toBeSavedWithInsert());
}
/**
* Determines whether or not a value stored in the record is clean.
* @return true if clean
* @throws DataSetException
*/
public function valueIsClean($column)
{
if (!isset($this->values[$column])) {
throw new DataSetException("Undefined column: ".$column);
}
return !isset($this->dirtyCols[$column]);
}
/**
* Goes through all the values in the record to determine if it is clean or not.
* @return true if clean
*/
public function recordIsClean()
{
return empty($this->dirtyCols);
}
/**
* This method refreshes this Record's Value's. It can only be performed on
* a Record that has not been modified and has been created with a TableDataSet
* and corresponding KeyDef.
*
* @param Connection $conn
* @throws DataSetException
* @throws SQLException
*/
public function refresh(Connection $conn = null)
{
if ($conn === null) {
$conn = $this->ds->connection();
}
if ($this->toBeSavedWithDelete()) {
return;
} elseif ($this->toBeSavedWithInsert()) {
throw new DataSetException("There is no way to refresh a record which has been created with addRecord().");
} elseif ($this->ds instanceof QueryDataSet) {
throw new DataSetException ("You can only perform a refresh on Records created with a TableDataSet.");
}
$stmt = null;
try {
$stmt = $conn->prepareStatement ($this->getRefreshSql());
$ps = 1;
$kd = $this->ds->keydef();
for ($i = 1, $kdsize = $kd->size(); $i <= $kdsize; $i++)
{
$val = $this->getValue($kd->getAttrib($i));
if ($val == null) {
throw new DataSetException ("You cannot execute an update with a null value for a KeyDef.");
}
$setter = 'set' . CreoleTypes::getAffix( $table->getColumn($col)->getType() );
$stmt->$setter($ps++, $val);
}
$rs = $stmt->executeQuery();
$rs->next();
$this->initializeRecord();
$this->createValues($rs);
} catch (SQLException $e) {
if ($stmt) $stmt->close();
throw $e;
}
}
/**
* This builds the SELECT statement in order to refresh the contents of
* this Record. It depends on a valid KeyDef to exist and it must have been
* created with a TableDataSet.
*
* @return string The SELECT SQL
* @throws DataSetException
*/
public function getRefreshSql()
{
if ($this->ds->keydef() === null || $this->ds->keydef()->size() === 0) {
throw new DataSetException("You can only perform a getRefreshQueryString on a TableDataSet that was created with a KeyDef.");
} elseif ($this->ds instanceof QueryDataSet) {
throw new DataSetException("You can only perform a getRefreshQueryString on Records created with a TableDataSet.");
}
$sql1 = "";
$sql2 = "";
$comma = false;
foreach($this->columns() as $col) {
if (!$comma) {
$attribs_sql .= $col;
$comma = true;
} else {
$attribs_sql .= ", " . $col;
}
}
$comma = false;
for ($i = 1, $kdsize = $kd->size(); $i <= $kdsize; $i++) {
$attrib = $kd->getAttrib($i);
if (!$this->valueIsClean($attrib)) {
throw new DataSetException (
"You cannot do a refresh from the database if the value " .
"for a KeyDef column has been changed with a Record.setValue().");
}
if (!$comma) {
$where_sql .= $attrib . " = ?";
$comma = true;
} else {
$where_sql .= " AND " . $attrib . " = ?";
}
}
return "SELECT " . $attribs_sql . " FROM " . $this->ds->tableName() . " WHERE " . $where_sql;
}
/**
* Gets the DataSet for this Record.
*
* @return DataSet
*/
public function dataset()
{
return $this->ds;
}
/**
* Sets the parent DataSet for this record.
* @param DataSet $ds
*/
public function setParentDataSet(DataSet $ds)
{
$this->ds = $ds;
}
/**
* This returns a representation of this Record.
* @return string
*/
public function __toString()
{
$sb = "{";
foreach($this->columns() as $col) {
$sb .= "'" . $this->getValue($col) . "',";
}
$sb = substr($sb, 0, -1);
$sb .= "}";
return $sb;
}
}