Location: PHPKode > projects > Recess PHP Framework > recess/recess/database/orm/Model.class.php
<?php
Library::import('recess.lang.Inflector');
Library::import('recess.lang.Object');
Library::import('recess.lang.reflection.RecessReflectionClass');
Library::import('recess.lang.Annotation');

Library::import('recess.database.Databases');
Library::import('recess.database.sql.ISqlConditions');
Library::import('recess.database.orm.ModelClassInfo');
Library::import('recess.database.sql.SqlBuilder');

Library::import('recess.database.orm.annotations.HasManyAnnotation', true);
Library::import('recess.database.orm.annotations.BelongsToAnnotation', true);
Library::import('recess.database.orm.annotations.DatabaseAnnotation', true);
Library::import('recess.database.orm.annotations.TableAnnotation', true);
Library::import('recess.database.orm.annotations.ColumnAnnotation', true);

Library::import('recess.database.orm.relationships.Relationship');
Library::import('recess.database.orm.relationships.HasManyRelationship');
Library::import('recess.database.orm.relationships.BelongsToRelationship');

/**
 * Model is the basic unit of organization in Recess' simple ORM.
 * 
 * @author Kris Jordan <hide@address.com>
 * @copyright 2008, 2009 Kris Jordan
 * @package Recess PHP Framework
 * @license MIT
 * @link http://www.recessframework.org/
 */
abstract class Model extends Object implements ISqlConditions {
	
	const CLASSNAME = 'Model';
	const INSERT = 'insert';
	const UPDATE = 'update';
	const DELETE = 'delete';
	const SAVE = 'save';
	
	/**
	 * Constructor can take either a keyed array or a string/int
	 * to set the primary key with;
	 *
	 * @param mixed $data
	 */
	final public function __construct($data = null) {
		if(is_numeric($data) || is_string($data)) {
			$primaryKey = Model::primaryKeyName($this);
			$this->$primaryKey = $data;
		} else if (is_array($data)) {
			$this->copy($data, false);
		}
	}
	
	/**
	 * Get the datasource for a class.
	 *
	 * @param mixed $class
	 * @return ModelDataSource
	 */
	static function sourceFor($class) {
		return self::getClassDescriptor($class)->getSource();
	}
	
	/**
	 * Get the name of the datasource for a class
	 *
	 * @param mixed $class
	 * @return string Key name of the ModelDataSource in Databases
	 */
	static function sourceNameFor($class) {
		return self::getClassDescriptor($class)->getSourceName();
	}
	
	/**
	 * The table which $modelClass is persisted on.
	 *
	 * @param mixed $class
	 * @return string Table Name
	 */
	static function tableFor($class) {
		return self::getClassDescriptor($class)->getTable();
	}
	
	/**
	 * Return the primary key column name for a class. This is prefixed
	 * with the class' table name.
	 *
	 * @param midex $class
	 * @return string Primary Key Column Name ie "table.id"
	 */
	static function primaryKeyFor($class) {
		$descriptor = self::getClassDescriptor($class);
		return $descriptor->getTable() . '.' . $descriptor->primaryKey;
	}
	
	/**
	 * Return the property name for the primary key.
	 *
	 * @param mixed $class
	 * @return string Primary key name ie: 'id'
	 */
	static function primaryKeyName($class) {
		return self::getClassDescriptor($class)->primaryKey;
	}
	
	/**
	 * Get a relationship on a class or instance by the relationship's name.
	 *
	 * @param mixed $classOrInstance
	 * @param string $name of the relationship
	 * @return Relationship
	 */
	static function getRelationship($classOrInstance, $name) {
		if(isset(self::getClassDescriptor($classOrInstance)->relationships[$name])) {
			return self::getClassDescriptor($classOrInstance)->relationships[$name];
		} else {
			return false;
		}
	}
	
	/**
	 * Return all relationships for a class or instance
	 *
	 * @param mixed $classOrInstance
	 * @return array of Relationship
	 */
	static function getRelationships($classOrInstance) {
		return self::getClassDescriptor($classOrInstance)->relationships;
	}
	
	/**
	 * Retrieve an array of column names in the table corresponding to
	 * a model class.
	 *
	 * @param mixed $classOrInstance
	 * @return array of strings of column names
	 */
	static function getColumns($classOrInstance) {
		return self::getClassDescriptor($classOrInstance)->columns;
	}
	
	/**
	 * Retrieve an array of the properties.
	 *
	 * @param mixed $classOrInstance
	 * @return array of type ModelProperty
	 */
	static function getProperties($classOrInstance) {
		return self::getClassDescriptor($classOrInstance)->properties;
	}

	protected static function initClassDescriptor($class) {	
		return new ModelDescriptor($class, false);
	}
	
	protected static function shapeDescriptorWithProperty($class, $property, $descriptor, $annotations) {
		if(!$property->isStatic() && $property->isPublic()) {
			$modelProperty = new ModelProperty();
			$modelProperty->name = $property->name;
			$descriptor->properties[$modelProperty->name] = $modelProperty;
		}
		return $descriptor;
	}
	
	protected static function finalClassDescriptor($class, $descriptor) {
		$modelSource = Databases::getSource($descriptor->getSourceName());
		$modelSource->cascadeTableDescriptor($descriptor->getTable(), $modelSource->modelToTableDescriptor($descriptor));	
		return $descriptor;
	}
	
	/**
	 * Attempt to generate a table from this model's descriptor.
	 *
	 * @param mixed $class
	 */
	static function createTableFor($class) {
		$descriptor = self::getClassDescriptor($class);
		$modelSource = Databases::getSource($descriptor->getSourceName());
		$modelSource->exec($modelSource->createTableSql($descriptor));
	}

	/**
	 * Build a ModelSet from this instance by assigning this Model instance's
	 * properties and values.
	 *
	 * @return ModelSet
	 */
	protected function getModelSet() {
		$thisClassDescriptor = self::getClassDescriptor($this);
		$result = $thisClassDescriptor->getSource()->selectModelSet($thisClassDescriptor->getTable());
		$pkName = self::primaryKeyName($this);
		
		if(isset($this->$pkName)) {
			$result = $result->equal($pkName,$this->$pkName);
		} else {
			foreach($this as $column => $value) {
				if(isset($this->$column) && in_array($column,$thisClassDescriptor->columns)) {
					$result = $result->assign($column, $value);
				}
			}
		}
		
		$result->rowClass = get_class($this);
		return $result;
	}
	
	/**
	 * Return a results ModelSet based on the values of this instance's properties.
	 *
	 * @return ModelSet
	 */
	function select() { 
		return $this->getModelSet()->useAssignmentsAsConditions(true);
	}

	/**
	 * Alias for select.
	 *
	 * @return ModelSet
	 */
	function find() { return $this->select(); }
	
	/**
	 * Select all. This is different from find() in that find will use
	 * assigned values to the model as equality statements.
	 *
	 * @return ModelSet
	 */
	function all() { 
		return $this->getModelSet();
	}
	
	
	/**
	 * Return a SqlBuilder object which has set the table and optionally
	 * assigned values to columns based on this instances' properties. This is used in
	 * insert(), update(), and delete()
	 *
	 * @param ModelDescriptor $descriptor
	 * @param boolean $useAssignment
	 * @param boolean $excludePrimaryKey
	 * @return SqlBuilder
	 */
	protected function assignmentSqlForThisObject(ModelDescriptor $descriptor, $useAssignment = true, $excludePrimaryKey = false) {
		$sqlBuilder = new SqlBuilder();
		$sqlBuilder->from($descriptor->getTable());
		
		if(empty($descriptor->columns)) {
			throw new RecessException('The "' . $descriptor->getTable() . '" table does not appear to exist in your database.', get_defined_vars());
		}
		
		foreach($this as $column => $value) {
			if($excludePrimaryKey && $descriptor->primaryKey == $column) continue;
			if(in_array($column, $descriptor->columns) && isset($value)) {
				if($useAssignment) {
					$sqlBuilder->assign($column,$value);
				} else {
					$sqlBuilder->equal($column,$value);
				}
			}
		}
		return $sqlBuilder;
	}
	
	/**
	 * Delete row(s) from the data source which match this instance.
	 *
	 * @param boolean $cascade - Also delete models related to this model?
	 * @return boolean
	 * 
	 * !Wrappable delete
	 */
	function wrappedDelete($cascade = true) {
		$thisClassDescriptor = self::getClassDescriptor($this);
		
		if($cascade) {
			foreach($thisClassDescriptor->relationships as $relationship) {
				$relationship->delete($this);
			}
		}
			
		$sqlBuilder = $this->assignmentSqlForThisObject($thisClassDescriptor, false);
		
		return $thisClassDescriptor->getSource()->executeSqlBuilder($sqlBuilder, 'delete');	
	}

	/**
	 * Insert row into the data source based on the values of this instance.
	 * @return boolean
	 * 
	 * !Wrappable insert
	 */
	function wrappedInsert() {
		$thisClassDescriptor = self::getClassDescriptor($this);
		
		$sqlBuilder = $this->assignmentSqlForThisObject($thisClassDescriptor);
		
		$result = $thisClassDescriptor->getSource()->executeSqlBuilder($sqlBuilder, 'insert');
		
	 	$primaryKey = $thisClassDescriptor->primaryKey;
	 	
	 	$this->$primaryKey = $thisClassDescriptor->getSource()->lastInsertId();
	 	
	 	return $result;
	}

	/**
	 * Update a row in the data source based on the values of this instance.
	 * @return boolean
	 * 
	 * !Wrappable update
	 */
	function wrappedUpdate() {
		$thisClassDescriptor = self::getClassDescriptor($this);
		
		$sqlBuilder = $this->assignmentSqlForThisObject($thisClassDescriptor, true, true);
		$primaryKey = $thisClassDescriptor->primaryKey;
		$sqlBuilder->equal($thisClassDescriptor->primaryKey, $this->$primaryKey);
		
		return $thisClassDescriptor->getSource()->executeSqlBuilder($sqlBuilder, 'update');
	}
	
	/**
	 * Insert or update depending on whether or not this instance's primary key is set.
	 *
	 * @return boolean
	 * 
	 * !Wrappable save
	 */
	function wrappedSave()   {
		if($this->primaryKeyIsSet()) {
			return $this->update();
		} else {
			return $this->insert();
		}
	}
	
	/**
	 * @return boolean
	 */
	function primaryKeyIsSet() {
		$thisClassDescriptor = self::getClassDescriptor($this);
		
		$primaryKey = $thisClassDescriptor->primaryKey;
				
		if(isset($this->$primaryKey)) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Shortcut method which will determine whether a row
	 * with the current instances properties exists. If so, it will
	 * preload those values (side effects).
	 * 
	 * Usage:
	 * $model->id = 1;
	 * if($model->exists()) {
	 *  die('a lonesome death');
	 * }
	 *
	 * @return boolean
	 */
	function exists() {
		$result = $this->select()->first();
		if($result !== false) {
			$this->copy($result, false);
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Copy values from a key/value array or another model/object
	 * to this instance.
	 *
	 * @param iterable $keyValuePair
	 * @return Model
	 */
	function copy($keyValuePair, $excludePrimaryKey = true) {
		if($excludePrimaryKey) {
			$pk = Model::primaryKeyName($this);
		}
		foreach($keyValuePair as $key => $value) {
			if($excludePrimaryKey && $key == $pk) {
				continue;
			}
			$this->$key = $value;
		}
		return $this;
	}
	
	/**
	 * Add equality criteria between a column and value
	 *
	 * @param string $lhs Column
	 * @param mixed $rhs Value
	 * @return PdoDataSet
	 */
	function equal($column, $rhs){ return $this->select()->equal($column, $rhs); }
	
	/**
	 * Add inequality criteria between a column and value
	 *
	 * @param string $lhs Column
	 * @param mixed $rhs Value
	 * @return PdoDataSet
	 */
	function notEqual($column, $rhs) { return $this->select()->notEqual($column,$rhs); }
	
	/**
	 * Add criteria to state a column's value falls between $small and $big
	 *
	 * @param string $column Column
	 * @param mixed $small Floor Value
	 * @param mixed $big Ceiling Value
	 * @return PdoDataSet
	 */
	function between ($column, $small, $big) { return $this->select()->between($column, $small, $big); }
	
	/**
	 * SQL criteria specifying a column's value is greater than $rhs
	 *
	 * @param string $column Column
	 * @param mixed $rhs Value
	 * @return PdoDataSet
	 */
	function greaterThan($column, $rhs) { return $this->select()->greaterThan($column,$rhs); }
	
	/**
	 * SQL criteria specifying a column's value is no less than $rhs
	 *
	 * @param string $column Column
	 * @param mixed $rhs Value
	 * @return PdoDataSet
	 */
	function greaterThanOrEqualTo($column, $rhs) { return $this->select()->greaterThanOrEqualTo($column,$rhs); }
	
	/**
	 * SQL criteria specifying a column's value is less than $rhs
	 *
	 * @param string $column Column
	 * @param mixed $rhs Value
	 * @return PdoDataSet
	 */
	function lessThan($column, $rhs) { return $this->select()->lessThan($column,$rhs); }
	
	/**
	 * SQL criteria specifying a column's value is no greater than $rhs
	 *
	 * @param string $column Column
	 * @param mixed $rhs Value
	 * @return PdoDataSet
	 */
	function lessThanOrEqualTo($column, $rhs) { return $this->select()->lessThanOrEqualTo($column,$rhs); }
	
	/**
	 * SQL LIKE criteria, note this does not automatically include wildcards
	 *
	 * @param string $column Column
	 * @param mixed $rhs Value
	 * @return PdoDataSet
	 */
	function like($column, $rhs) { return $this->select()->like($column,$rhs); }
	
	/**
	 * SQL NOT LIKE criteria, note this does not automatically include wildcards
	 *
	 * @param string $column Column
	 * @param mixed $rhs Value
	 * @return PdoDataSet
	 */
	function notLike($column, $rhs) { return $this->select()->notLike($column,$rhs); }
	
	/**
	 * SQL IS NULL criteria
	 *
	 * @param string $column Column
	 * @return PdoDataSet
	 */
	function isNull($column) { return $this->select()->isNull($column); }
	
	/**
	 * SQL IS NOT NULL criteria
	 *
	 * @param string $column Column
	 * @return PdoDataSet
	 */
	function isNotNull($column) { return $this->select()->isNotNull($column); }
	
}

/**
 * Class descriptor + metadata for a model.
 */
class ModelDescriptor extends ClassDescriptor {
	
	public $primaryKey = 'id';
	private $table;
	
	public $plural;
	public $modelClass;
	public $relationships;
	
	public $columns;
	public $properties;
	
	public $source;
	
	function __construct($class, $loadColumns = true) {
		$this->table = strtolower($class);
		$this->relationships = array();
		$this->properties = array();
		$this->source = false;
		if($loadColumns) {
			$this->columns = $this->getSource()->getColumns($this->table);
		} else {
			$this->columns = array();
		}
		$this->primaryKeyColumn = 'id';
		$this->modelClass = $class;
	}
	
	function __set_state($array) {
		$descriptor = new ModelDescriptor($array['modelClass']);
		$descriptor->primaryKey = $array['primaryKey'];
		$descriptor->table = $array['table'];
		$descriptor->relationships = $array['relationships'];
		$descriptor->columns = $array['columns'];
		$descriptor->properties = $array['properties'];
		$descriptor->source = $array['source'];
		$descriptor->attachedMethods = $array['attachedMethods'];
		return $descriptor;
	}
	
	function setTable($table, $loadColumns = true) {
		$this->table = $table;
		if($loadColumns) {
			$source = $this->getSource();
			if(isset($source)) {
				$this->columns = $this->getSource()->getColumns($this->table);
			} else {
				throw new RecessException('Data Source "' . $this->getSourceName() . '" is not set.', array());
			}
		} else {
			$this->columns = array();
		}
	}
	
	function getTable() {
		return $this->table;
	}
	
	function setSource($source) {
		$this->source = $source;		
	}
	
	function getSource() {
		if(!$this->source) {
			return Databases::getDefaultSource();
		} else {
			return Databases::getSource($this->source);
		}
	}
	
	function getSourceName() {
		if(!$this->source) {
			return 'Default';
		} else {
			return $this->source;
		}
	}
}

/**
 * The data structure for a propery on a model
 */
class ModelProperty {
	public $name;
	public $type;
	public $pkCallback;
	public $isAutoIncrement = false;
	public $isPrimaryKey = false;
	public $isForeignKey = false;
	public $required = false;
	
	function __set_state($array) {
		$prop = new ModelProperty();
		$prop->name = $array['name'];
		$prop->type = $array['type'];
		$prop->pkCallback = $array['pkCallback'];
		$prop->isAutoIncrement = $array['autoincrement'];
		$prop->isPrimaryKey = $array['isPrimaryKey'];
		$prop->isForeignKey = $array['isForeignKey'];
		return $prop;
	}
}
?>
Return current item: Recess PHP Framework