<?php

/**
 * Active Record
 *
 * Persistent object stored as a record in database table.
 * Handles 1-to-many and many-to-many relations.
 *
 * @version DR1.6
 */
abstract class ActiveRecord implements IteratorAggregate {



	protected $tableName;
	protected $primaryKey = 'id';
	protected $sequenceName = '';

	protected $data;
	protected $modified = false;

	protected $dbAdapter;

	protected $hasMany = array(); // array(field => array(className, foreignKey in this class))
	protected $properties = array();

	protected $belongsTo = array(); // array(field => array(className, foreignKey in className class))
	protected $owners = array();

	protected $manyToMany = array(); // array(field => array(className, mapName, mapPrimaryKey, mapForeignKey))
	protected $m2mCollections = array();



	public function __construct() {
		$this->data = new ArrayObject();
		$this->dbAdapter = $this->buildDBAdapter();
	}



	/**
	 * Database adapter factory method
	 *
	 * Returns DBAdapter instance.
	 * Override this method if you wish to use adapter different than
	 * the one provided by <code>DBFactory::getAdapter()</code> method.
	 *
	 * @return DBAdapter
	 */
	protected function buildDBAdapter() {
		return DBFactory::getAdapter();
	}



	/**
	 * Loads data from database table record by primary key
	 */
	public function loadById($id) {
		$this->loadByFieldValue($this->primaryKey, $id);
	}



	/**
	 * Loads data from database table record by any field value
	 */
	public function loadByFieldValue($k, $v) {
		$dialect = $this->dbAdapter->getDialect();
		$q = $dialect->getQuotes();
		return $this->loadBySQLCondition($q . $k . $q . " = '$v'");
	}



	public function loadBySQLCondition($condition, $orderBy = null) {
		$order = $orderBy ? $orderBy : $this->primaryKey;
		$sql = "
				SELECT * FROM {$this->tableName}
				WHERE $condition
		";
		try {
			$this->dbAdapter->execute($sql);
		} catch (Exception $e) {
			throw new Exception("db query failed: $sql");
		}
		$i = 0;
		$selection = new ArrayObject();
		$className = get_class($this);
		foreach ($this->dbAdapter as $row) {
			$pkey = $row[$this->primaryKey];
			if (!is_numeric($pkey) and !is_string($pkey)) {
				throw new Exception(get_class($this) . " got illegal primary key value: $sql");
			}
			if ($i) {
				$selection[$pkey] = new $className();
			} else {
				$selection[$pkey] = $this;
			}
			$selection[$pkey]->data = new ArrayObject($row);
			++$i;
		}
		return $selection;
	}



	/**
	 * Stores internal data and relations into database table record
	 */
	public function save() {
		if ($this->modified) {
			$dialect = $this->dbAdapter->getDialect();
			if (array_key_exists($this->primaryKey, $this->data) and $this->data[$this->primaryKey]) {
				$sql = $dialect->getUpdate($this->tableName, $this->data, $this->primaryKey, $this->data[$this->primaryKey]);
				$out = $this->dbAdapter->execute($sql);
			} else {
				$sql = $dialect->getInsert($this->tableName, $this->data);
				$sequenceName = $this->sequenceName ? $this->sequenceName : ($this->tableName . '_' . $this->primaryKey . '_seq'); // hack for PostgreSQL
				if ($out = $this->dbAdapter->execute($sql, $sequenceName)) {
					$this->data[$this->primaryKey] = $out;
				}
			}
			if (!$out) {
				throw new Exception('saving ' . get_class($this) . ' failed: ' . $sql);
			}
			$this->modified = false;
		}
		foreach ($this->owners as $owner) {
			$owner->save();
		}
		foreach ($this->properties as $properties) {
			foreach ($properties as $property) {
				$property->save();
			}
		}
	}



	/**
	 * Removes record from database
	 */
	public function delete() {
		$keyValue = $this->getPrimaryKeyValue();
		if ($keyValue === null) {
			return;
		}
		// remove all 1-to-many relations from dependant objects
		foreach ($this->belongsTo as $field => $definition) {
			$this->unsetBelongsTo($field);
		}
		// remove all 1-to-many relations
		foreach ($this->hasMany as $field => $definition) {
			foreach ($this->$field as $ar) {
				$this->$field->remove($ar);
			}
		}
		// remove all many-to-many relations
		foreach ($this->manyToMany as $field => $definition) {
			foreach ($this->$field as $ar) {
				$this->$field->remove($ar);
			}
		}
		// then remove self
		$dialect = $this->dbAdapter->getDialect();
		$sql = $dialect->getDelete($this->tableName, $this->primaryKey, $keyValue);
		$out = $this->dbAdapter->execute($sql);
		if (!$out) {
			throw new Exception('deleting ' . get_class($this) . ' failed: ' . $sql);
		}
		unset($this->data[$this->primaryKey]);
	}



	public function getPrimaryKeyName() {
		return $this->primaryKey;
	}



	public function getPrimaryKeyValue() {
		return (array_key_exists($this->primaryKey, $this->data)) ? $this->data[$this->primaryKey] : null;
	}



	public function getTableName() {
		return $this->tableName;
	}



	public function getRecord() {
		return $this->data;
	}



	public function setRecord($r, $overridePrimaryKey = false) {
		if (!is_array($r) and !($r instanceof Iterator) and !($r instanceof IteratorAggregate)) {
			throw new Exception('array, Iterator or IteratorAggregate expected');
		}
		foreach ($r as $field => $value) {
			if (array_key_exists($field, $this->belongsTo)) {
				$className = $this->belongsTo[$field][0];
				$ar = &new $className();
				$ar->loadById($value);
				$this->setBelongsTo($field, $ar);
			} elseif (array_key_exists($field, $this->hasMany)) {
				$className = $this->hasMany[$field][0];
				$ar = &new $className();
				$ar->loadById($value);
				$this->$field->add($ar);
			} elseif (array_key_exists($field, $this->manyToMany)) {
				$className = $this->manToMany[$field][0];
				$ar = &new $className();
				$ar->loadById($value);
				$this->$field->add($ar);
			} elseif ($field != $this->primaryKey or $overridePrimaryKey) {
				$this->data[$field] = $value;
			}
		}
	}



	public function getIterator() {
		return $this->data->getIterator();
	}



	public function __get($field) {
		if (array_key_exists($field, $this->belongsTo)) {
			return $this->getBelongsTo($field);
		}
		if (array_key_exists($field, $this->hasMany)) {
			return $this->getHasMany($field);
		}
		if (array_key_exists($field, $this->manyToMany)) {
			return $this->getM2M($field);
		}
		if (array_key_exists($field, $this->data)) {
			return $this->data[$field];
		}
		return null;
	}



	public function __set($field, $value) {
		if (array_key_exists($field, $this->belongsTo)) {
			$this->setBelongsTo($field, $value);
		} elseif (array_key_exists($field, $this->hasMany) or array_key_exists($field, $this->manyToMany)) {
			throw new Exception('use add() to add item to hasMany or manyToMany relation');
		} else {
			$this->data[$field] = $value;
		}
		$this->modified = true;
	}



	public function __unset($field) {
		if (array_key_exists($field, $this->belongsTo)) {
			$this->unsetBelongsTo($field);
		} elseif (array_key_exists($field, $this->hasMany) or array_key_exists($field, $this->manyToMany)) {
			throw new Exception('use remove() to remove item from hasMany or manyToMany relation');
		} else {
			unset($this->data[$field]);
		}
		$this->modified = true;
	}



	protected function getHasMany($field) {
		if (!array_key_exists($field, $this->properties)) {
			list($className, $foreignKey) = $this->hasMany[$field];
			$this->properties[$field] = OneToManyRelation::getInstance($this, $className, $foreignKey, $this->dbAdapter);
		}
		return $this->properties[$field];
	}



	protected function getBelongsTo($field) {
		if (!array_key_exists($field, $this->owners)) {
			list($className, $foreignKey) = $this->belongsTo[$field];
			if ($this->$foreignKey === null) {
				return null;
			}
			$owner = new $className();
			$owner->loadById($this->data[$foreignKey]);
			$this->owners[$field] = $owner;
		}
		return $this->owners[$field];
	}



	protected function setBelongsTo($field, ActiveRecord $ar) {
		list($className, $foreignKey) = $this->belongsTo[$field];
		if (!($ar instanceof $className)) {
			throw new Exception("expected $className while setting owner, got " . get_class($ar));
		}
		if (!$ar->getPrimaryKeyValue()) {
			$ar->save();
		}
		$this->$foreignKey = $ar->getPrimaryKeyValue();
		$this->save();
		$this->owners[$field] = $ar;
	}



	protected function unsetBelongsTo($field) {
		list($className, $foreignKey) = $this->belongsTo[$field];
		$this->$foreignKey = null;
		if (array_key_exists($field, $this->owners)) {
			unset($this->owners[$field]);
		}
		$this->save();
	}



	protected function getM2M($field) {
		if (!array_key_exists($field, $this->m2mCollections)) {
			$this->m2mCollections[$field] = new ManyToManyRelation(
				$this,
				$this->manyToMany[$field][0],
				$this->manyToMany[$field][1],
				$this->manyToMany[$field][2],
				$this->manyToMany[$field][3]
			);
			$this->m2mCollections[$field]->setDBAdapter($this->dbAdapter);
		}
		return $this->m2mCollections[$field];
	}



} // class ActiveRecord

?>