* //plain vanilla instance
* $b = new Book();
* //fills instance with passed properties
* $b = new Book(array('title' => 'Alice in Wonderland'));
* //tries to load instance from database using 1 as a primary key identifier
* $b = new Book(1);
*
* @param array|integer Depending on argument type the new object is filled with properties or loaded from database
*/
function __construct($magic_params = null, $conn = null)
{
parent :: __construct();
$this->_defineRelations();
if(is_object($conn))
$this->_db_conn = $conn;
else
$this->_db_conn = self :: getDefaultConnection();
$this->_db_meta_info = lmbToolkit :: instance()->getActiveRecordMetaInfo($this, $this->_db_conn);
$this->_db_table = $this->_db_meta_info->getDbTable();
$this->_db_table_name = $this->_db_table->getTableName();
$this->_error_list = new lmbErrorList();
if(is_int($magic_params))
$this->loadById($magic_params);
elseif(is_array($magic_params) || is_object($magic_params))
$this->import($magic_params);
}
/**
* Sets database resource identifier used for database access
* @param string DSN, e.g. mysql://root:secret@localhost/mydb
*/
static function setDefaultDSN($dsn)
{
self :: $_default_db_conn = lmbToolkit :: instance()->createDbConnection($dsn);
}
/**
* Sets default database connection object
* @param object instance of concrete lmbDbConnection interface implementation
* @return object previous connection object
* @see lmbDbConnection
*/
static function setDefaultConnection($conn)
{
$prev = self :: $_default_db_conn;
self :: $_default_db_conn = $conn;
return $prev;
}
/**
* Returns current default database connection object
* @return object instance of concrete lmbDbConnection interface implementation
* @see lmbDbConnection
*/
static function getDefaultConnection()
{
if(is_object(self :: $_default_db_conn))
return self :: $_default_db_conn;
return lmbToolkit :: instance()->getDefaultDbConnection();
}
/**
* Returns current single table inheritance column name
* @return string
*/
static function getInheritanceField()
{
return self :: $_inheritance_field;
}
/**
* Allows to override default single table inheritance column name
* @param string
*/
static function setInheritanceField($field)
{
return self :: $_inheritance_field = $field;
}
/**
* Returns name of database table
* @return string
*/
function getTableName()
{
return $this->_db_table_name;
}
/**
* Returns table gateway instance used for all db interactions
* @return object
*/
function getDbTable()
{
return $this->_db_table;
}
/**
* Returns error list object with all validation errors
* @return object
*/
function getErrorList()
{
return $this->_error_list;
}
protected function _defineRelations(){}
protected function _hasOne($relation_name, $info)
{
$this->_has_one[$relation_name] = $info;
}
protected function _hasMany($relation_name, $info)
{
$this->_has_many[$relation_name] = $info;
}
protected function _hasManyToMany($relation_name, $info)
{
$this->_has_many_to_many[$relation_name] = $info;
}
protected function _belongsTo($relation_name, $info)
{
$this->_belongs_to[$relation_name] = $info;
}
protected function _manyBelongsTo($relation_name, $info)
{
$this->_many_belongs_to[$relation_name] = $info;
}
protected function _composedOf($relation_name, $info)
{
$this->_composed_of[$relation_name] = $info;
}
/**
* Returns relation info array defined during class declaration
* @return array
*/
function getRelationInfo($relation)
{
$relations = $this->_getAllRelations();
if(isset($relations[$relation]))
return $relations[$relation];
}
protected function _getAllRelations()
{
return array_merge($this->_has_one,
$this->_has_many,
$this->_has_many_to_many,
$this->_belongs_to,
$this->_many_belongs_to,
$this->_composed_of);
}
/**
* Returns all relations info for one-to-many
* @return array
*/
function getOneToManyRelationsInfo()
{
return $this->_has_many;
}
/**
* Returns all relations info for many-to-many
* @return array
*/
function getManyToManyRelationsInfo()
{
return $this->_has_many_to_many;
}
/**
* Returns all relations info for belongs-to
* @return array
*/
function getBelongsToRelationsInfo()
{
return $this->_belongs_to;
}
/**
* Returns all relations info for many-belongs-to
* @return array
*/
function getManyBelongsToRelationsInfo()
{
return $this->_many_belongs_to;
}
/**
* Returns default sort params
* @return array
*/
function getDefaultSortParams()
{
if(!$this->_default_sort_params)
$this->_default_sort_params = array($this->_db_table_name . '.id' => 'ASC');
return $this->_default_sort_params;
}
protected function _createTableObjectByAlias($class_path_alias)
{
$class_path = new lmbClassPath($class_path_alias);
return $class_path->createObject();
}
/**
* Returns common validator for create and update operations. It should be overridden
* if you want to have a custom validator, e.g:
*
*
* $validator = new lmbValidator();
* $validator->addRequiredRule('title');
* return $validator;
*
* @return object
*/
protected function _createValidator()
{
return new lmbValidator();
}
/**
* Returns validator for create operations only.
* @see _createValidator()
* @return object
*/
protected function _createInsertValidator()
{
return $this->_createValidator();
}
/**
* Returns validator for update operations only.
* @see _createValidator()
* @return object
*/
protected function _createUpdateValidator()
{
return $this->_createValidator();
}
protected function _savePreRelations()
{
foreach($this->_has_one as $property => $info)
$this->_savePreRelationObject($property, $info, true);
foreach($this->_many_belongs_to as $property => $info)
$this->_savePreRelationObject($property, $info, false);
}
protected function _savePreRelationObject($property, $info, $save_relation_obj = true)
{
if($this->isDirtyProperty($info['field']) && !$this->isDirtyProperty($property))
{
$value = $this->_getRaw($info['field']);
if(is_null($value))
$this->_setRaw($property, null);
return;
}
$object = $this->_getRaw($property);
if(is_object($object))
{
if($object->isNew() || (!$object->isNew() && $save_relation_obj))
$object->save($this->_error_list);
$object_id = $object->getId();
if($this->_getRaw($info['field']) != $object_id)
$this->_setRaw($info['field'], $object->getId());
}
elseif(is_null($object) && $this->isDirtyProperty($property) &&
isset($info['can_be_null']) && $info['can_be_null'])
$this->_setRaw($info['field'], null);
}
protected function _savePostRelations()
{
foreach($this->_has_many as $property => $info)
$this->_savePostRelationCollection($property, $info);
foreach($this->_has_many_to_many as $property => $info)
$this->_savePostRelationCollection($property, $info);
foreach($this->_belongs_to as $property => $info)
$this->_savePostRelationObject($property, $info);
}
protected function _savePostRelationCollection($property, $info)
{
$collection = $this->_getRaw($property);
if(is_object($collection))
$collection->save($this->_error_list);
}
protected function _savePostRelationObject($property, $info)
{
$object = $this->_getRaw($property);
if(is_object($object))
{
$object->set($info['field'], $this->getId());
$object->save($this->_error_list);
}
}
protected function __call($method, $args = array())
{
if($property = $this->_mapGetToProperty($method))
return $this->get($property);
if($property = $this->mapAddToProperty($method))
{
$this->_addToProperty($property, $args[0]);
return;
}
return parent :: __call($method, $args);
}
protected function _addToProperty($property, $value)
{
$collection = $this->get($property);
if(!is_object($collection))
throw new lmbARException("Collection object info for property '$property' is missing");
$collection->add($value);
}
protected function _izLazyAttribute($property)
{
return in_array($property, $this->_lazy_attributes);
}
protected function _hasLazyAttributes()
{
if(!$this->_lazy_attributes)
return false;
foreach($this->_lazy_attributes as $attribute)
if(!$this->hasAttribute($attribute))
return true;
return false;
}
protected function _loadLazyAttribute($property)
{
$record = $this->_db_table->selectRecordById($this->getId(), array($property));
$processed = $this->_decodeDbValues($record);
$this->_setDbValue($property, $processed[$property]);
}
protected function _loadLazyAttributes()
{
foreach($this->_lazy_attributes as $attribute)
$this->_loadLazyAttribute($attribute);
}
/**
* Generic magic getter for any attribute
* @param string property name
* @return mixed
*/
function get($property)
{
if(!$this->isNew() && $this->_izLazyAttribute($property) && !$this->hasAttribute($property))
$this->_loadLazyAttribute($property);
if($this->_hasValueObjectRelation($property))
return $this->_getValueObject($property);
$value = parent :: get($property);
if(isset($value))
return $value;
if(!$this->isNew() && $this->_hasBelongsToRelation($property))
{
$object = $this->_loadBelongsToObject($property);
$this->_setRaw($property, $object);
return $object;
}
if(!$this->isNew() && $this->_hasManyBelongsToRelation($property))
{
$object = $this->_loadManyBelongsToObject($property);
$this->_setRaw($property, $object);
return $object;
}
if(!$this->isNew() && $this->_hasOneToOneRelation($property))
{
$object = $this->_loadOneToOneObject($property);
$this->_setRaw($property, $object);
return $object;
}
if($this->_hasCollectionRelation($property))
{
$collection = $this->createRelationCollection($property);
$this->_setRaw($property, $collection);
return $collection;
}
$exists = false;
}
function createRelationCollection($relation, $criteria = null)
{
$info = $this->getRelationInfo($relation);
if(isset($info['collection']))
return new $info['collection']($relation, $this, $criteria);
elseif($this->_hasOneToManyRelation($relation))
return new lmbAROneToManyCollection($relation, $this, $criteria, $this->_db_conn);
else if($this->_hasManyToManyRelation($relation))
return new lmbARManyToManyCollection($relation, $this, $criteria, $this->_db_conn);
}
protected function _hasCollectionRelation($relation)
{
return $this->_hasOneToManyRelation($relation) ||
$this->_hasManyToManyRelation($relation);
}
/**
* Generic magis getter for any attribute
* @param string property name
* @param mixed property value
*/
function set($property, $value)
{
if($this->_hasCollectionRelation($property))
{
if($this->isNew())
{
$collection = $this->createRelationCollection($property);
$this->_setRaw($property, $collection);
}
else
$collection = $this->get($property);
$collection->set($value);
}
else
parent :: set($property, $value);
}
protected function _setRaw($property, $value)
{
parent :: _setRaw($property, $value);
$this->_markDirtyProperty($property);
}
protected function _markDirtyProperty($property)
{
if(!$this->_canPropertyBeDirty($property))
return;
$this->_is_dirty = true;
$this->_dirty_props[$property] = 1;
}
protected function _canPropertyBeDirty($property)
{
if($this->_db_meta_info->hasColumn($property))
return true;
if($this->_canRelationPropertyBeDirty($property, $this->_many_belongs_to))
return true;
if($this->_canRelationPropertyBeDirty($property, $this->_has_one))
return true;
return false;
}
protected function _canRelationPropertyBeDirty($property, $info)
{
if(!isset($info[$property]))
return false;
if(($object = $this->_getRaw($property)) &&
($object->getId() == $this->_getRaw($info[$property]['field'])))
return false;
else
return true;
}
function resetDirty()
{
$this->_resetDirty();
}
protected function _resetDirty()
{
$this->_is_dirty = false;
$this->_dirty_props = array();
}
/**
* Marks object as dirty
*/
function markDirty()
{
$this->_is_dirty = true;
}
/**
* Returns object's dirtiness status
* @return boolean
*/
function isDirty()
{
return $this->_is_dirty;
}
/**
* Returns object's property dirtiness status
* @param string
* @return boolean
*/
function isDirtyProperty($property)
{
return isset($this->_dirty_props[$property]);
}
/**
* Maps property name to "addTo" form, e.g. "property_name" => "addToPropertyName"
* @param string
* @return string
*/
function mapPropertyToAddToMethod($property)
{
return 'addTo' . lmb_camel_case($property);
}
/**
* Maps "addTo" to property, e.g. "addToPropertyName" => "property_name"
* @param string
* @return string
*/
function mapAddToProperty($method)
{
if(substr($method, 0, 5) == 'addTo')
return lmb_under_scores(substr($method, 5));
}
/**
* Maps database field to property name
* @param string
* @return string
*/
function mapFieldToProperty($field)
{
foreach($this->_getAllRelations() as $property => $info)
{
if(isset($info['field']) && $info['field'] == $field)
return $property;
}
}
protected function _hasBelongsToRelation($property)
{
return isset($this->_belongs_to[$property]);
}
protected function _hasManyBelongsToRelation($property)
{
return isset($this->_many_belongs_to[$property]);
}
protected function _hasOneToOneRelation($property)
{
return isset($this->_has_one[$property]);
}
protected function _hasOneToManyRelation($property)
{
return isset($this->_has_many[$property]);
}
protected function _hasManyToManyRelation($property)
{
return isset($this->_has_many_to_many[$property]);
}
protected function _hasValueObjectRelation($property)
{
return isset($this->_composed_of[$property]);
}
protected function _loadBelongsToObject($property)
{
return self :: findFirst($this->_belongs_to[$property]['class'],
array('criteria' => $this->_belongs_to[$property]['field'] . ' = ' . (int)$this->getId()),
$this->_db_conn);
}
protected function _loadManyBelongsToObject($property)
{
$value = $this->_getRaw($this->_many_belongs_to[$property]['field']);
if(!$value && $this->_canManyBelongsToObjectBeNull($property))
return null;
return self :: findById($this->_many_belongs_to[$property]['class'],
$this->get($this->_many_belongs_to[$property]['field']),
$this->_db_conn);
}
protected function _loadOneToOneObject($property)
{
$value = $this->_getRaw($this->_has_one[$property]['field']);
if(!$value && $this->_canHasOneObjectBeNull($property))
return null;
return self :: findById($this->_has_one[$property]['class'],
$this->get($this->_has_one[$property]['field']),
$this->_db_conn);
}
protected function _canHasOneObjectBeNull($property)
{
return isset($this->_has_one[$property]['can_be_null']) &&
$this->_has_one[$property]['can_be_null'];
}
protected function _canManyBelongsToObjectBeNull($property)
{
return isset($this->_many_belongs_to[$property]['can_be_null']) &&
$this->_many_belongs_to[$property]['can_be_null'];
}
protected function _loadValueObject($property)
{
if(!isset($this->raw_value_objects[$this->_composed_of[$property]['field']]))
return null;
$value = $this->raw_value_objects[$this->_composed_of[$property]['field']];
return $this->_createValueObject($this->_composed_of[$property]['class'],
$value);
}
protected function _createValueObject($class, $value)
{
$object = new $class($value);
return $object;
}
protected function _mapMethodToClass($method)
{
return substr($method, 3);
}
protected function _getValueObject($property)
{
$value = $this->_getRaw($property);
if(!is_object($value))
{
$object = $this->_loadValueObject($property);
$this->_setRaw($property, $object);
return $object;
}
return $value;
}
protected function _doSave($need_validation)
{
if($this->_is_being_saved)
return;
try
{
$this->_is_being_saved = true;
$this->_savePreRelations();
$this->_onBeforeSave();
$this->_invokeListeners(self :: ON_BEFORE_SAVE);
if(!$this->isNew() && $this->isDirty())
{
$this->_onBeforeUpdate();
$this->_invokeListeners(self :: ON_BEFORE_UPDATE);
if($need_validation && !$this->_validateUpdate())
throw new lmbValidationException('ActiveRecord "' . get_class($this) . '" validation failed',
$this->_error_list);
$this->_onSave();
$this->_onUpdate();
$this->_invokeListeners(self :: ON_UPDATE);
$this->_setAutoTimes();
$this->_updateDbRecord($this->_propertiesToDbFields());
$this->_onAfterUpdate();
$this->_invokeListeners(self :: ON_AFTER_UPDATE);
}
elseif($this->isNew())
{
$this->_onBeforeCreate();
$this->_invokeListeners(self :: ON_BEFORE_CREATE);
if($need_validation && !$this->_validateInsert())
throw new lmbValidationException('ActiveRecord "' . get_class($this) . '" validation failed',
$this->_error_list);
$this->_onSave();
$this->_onCreate();
$this->_invokeListeners(self :: ON_CREATE);
$this->_setAutoTimes();
$new_id = $this->_insertDbRecord($this->_propertiesToDbFields());
$this->_is_new = false;
$this->setId($new_id);
$this->_onAfterCreate();
$this->_invokeListeners(self :: ON_AFTER_CREATE);
}
$this->_onAfterSave();
$this->_invokeListeners(self :: ON_AFTER_SAVE);
$this->_savePostRelations();
$this->_resetDirty();
$this->_is_being_saved = false;
}
catch(Exception $e)
{
$this->_db_conn->rollbackTransaction();
throw $e;
}
return $this->getId();
}
protected function _updateDbRecord($values)
{
return $this->_db_table->updateById($this->id, $values);
}
protected function _insertDbRecord($values)
{
return $this->_db_table->insert($values);
}
protected function _propertiesToDbFields()
{
$fields = $this->export();
if($this->isNew() && $this->_isInheritable())
$fields[self :: $_inheritance_field] = $this->_getInheritancePath();
foreach($this->_composed_of as $property => $info)
{
$object = $this->_getValueObject($property);
if(is_object($object))
{
$method = $info['getter'];
$fields[$info['field']] = $object->$method();
}
}
return $fields;
}
protected function _setAutoTimes()
{
if($this->isNew() && $this->_hasCreateTime())
$this->_setRaw(self :: $_ctime_field, time());
if($this->_hasUpdateTime())
$this->_setRaw(self :: $_utime_field, time());
}
protected function _hasUpdateTime()
{
return $this->_db_meta_info->hasColumn(self :: $_utime_field);
}
protected function _hasCreateTime()
{
return $this->_db_meta_info->hasColumn(self :: $_ctime_field);
}
protected function _isInheritable()
{
if(!is_null($this->_is_inheritable))
return $this->_is_inheritable;
$this->_is_inheritable = $this->_db_meta_info->hasColumn(self :: $_inheritance_field);
return $this->_is_inheritable;
}
/**
* Validates object and saves into database, throws exception if there were any errors
* @param object error list object which will receive all validation errors
* @return integer id of the saved object
*/
function save($error_list = null)
{
if($error_list)
$this->_error_list = $error_list;
return $this->_doSave(true);
}
/**
* Saves object into database skipping any validation, throws exception if there were any errors
* @return integer id of the saved object
*/
function saveSkipValidation()
{
return $this->_doSave(false);
}
/**
* Validates object and saves into database, catches all exceptions if there were any errors
* @param object error list object which will receive all validation errors
* @return boolean success status of operation
*/
function trySave($error_list = null)
{
try
{
$this->save($error_list);
}
catch(lmbValidationException $e)
{
return false;
}
catch(Exception $e)
{
if($error_list)
$error_list->addError('ActiveRecord :: save() exception: ' . $e->getMessage());
return false;
}
return true;
}
/**
* Returns whether object is new
* @return boolean
*/
function isNew()
{
return ($this->_is_new || !$this->getId());
}
/**
* Forces object to be new or not
* @param boolean new status
*/
function setIsNew($value = true)
{
$this->_is_new = (boolean)$value;
}
/**
* Detaches object by making it new and removing its identity
*/
function detach()
{
$this->setIsNew();
$this->remove('id');
}
/**
* Validates object
* @param object error list object which will receive all validation errors
* @return boolean validation status
*/
function validate($error_list = null)
{
if($error_list)
$this->_error_list = $error_list;
if($this->isNew())
return $this->_validateInsert();
else
return $this->_validateUpdate();
}
protected function _onBeforeUpdate(){}
protected function _onBeforeCreate(){}
protected function _onBeforeSave(){}
protected function _onBeforeDestroy(){}
protected function _onAfterSave(){}
protected function _onUpdate(){}
protected function _onCreate(){}
protected function _onSave(){}
protected function _onAfterUpdate(){}
protected function _onAfterCreate(){}
protected function _onAfterDestroy(){}
protected function _onValidate(){}
protected function _validateInsert()
{
return $this->_validate($this->_createInsertValidator());
}
protected function _validateUpdate()
{
return $this->_validate($this->_createUpdateValidator());
}
protected function _validate($validator)
{
$validator->setErrorList($this->_error_list);
$validator->validate($this);
$this->_onValidate();
return $this->_error_list->isValid();
}
protected function _addError($message, $fields = array(), $values = array())
{
$this->_error_list->addError($message, $fields, $values);
}
function isValid()
{
return $this->_error_list->isValid();
}
protected static function _isCriteria($params)
{
if(is_object($params) || is_string($params))
return true;
if(is_array($params) && sizeof($params))
{
foreach($params as $key => $value)
{
if(!is_int($key) || $value == 'first')
return false;
}
return true;
}
return false;
}
/**
* Finds one instance of object in database, this method is actually a wrapper around find()
* @see find()
* @param string class name of the object
* @param mixed misc magic params
* @param object database connection object
* @return object|null
*/
static function findFirst($class_name, $magic_params = array(), $conn = null)
{
$params = array();
if(self :: _isCriteria($magic_params))
$params = array('first', 'criteria' => $magic_params);
elseif(is_array($magic_params))
{
$params = $magic_params;
array_push($params, 'first');
}
if(!class_exists($class_name, true))
throw new lmbARException("Could not find class '$class_name'");
if(!is_object($conn))
$conn = self :: getDefaultConnection();
$obj = new $class_name(null, $conn);
return $obj->_findFirst($params);
}
/**
* self :: findFirst() convenience alias
* @see findFirst()
* @param string class name of the object
* @param mixed misc magic params
* @return object|null
*/
static function findOne($class_name, $magic_params = array(), $conn = null)
{
return self :: findFirst($class_name, $magic_params, $conn);
}
/**
* Userland filter for findFirst() static method
* @see findFirst()
* @param mixed misc magic params
* @return object|null
*/
protected function _findFirst($params)
{
return self :: find(get_class($this), $params, $this->_db_conn);
}
/**
* Finds one instance of object in database using object id, this method is actually a wrapper around find()
* @see find()
* @param string class name of the object
* @param integer object id
* @param object database connection object
* @return object|null
*/
static function findById($class_name, $id, $throw_exception = true, $conn = null)
{
if(!class_exists($class_name, true))
throw new lmbARException("Could not find class '$class_name'");
if(!is_object($conn))
$conn = self :: getDefaultConnection();
$obj = new $class_name(null, $conn);
return $obj->_findById($id, $throw_exception);
}
/**
* Userland filter for findById() static method
* @see findById()
* @param integer object id
* @return object
*/
protected function _findById($id, $throw_exception)
{
if($object = self :: find(get_class($this),
array('first', 'criteria' => 'id=' . (int)$id),
$this->_db_conn))
return $object;
elseif($throw_exception)
throw new lmbARNotFoundException(get_class($this), $id);
else
return null;
}
/**
* Finds a collection of objects in database using array of object ids, this method is actually a wrapper around find()
* @see find()
* @param string class name of the object
* @param array object ids
* @param mixed misc magic params
* @param object database connection object
* @return iterator
*/
static function findByIds($class_name, $ids, $params = array(), $conn = null)
{
if(!class_exists($class_name, true))
throw new lmbARException("Could not find class '$class_name'");
if(!is_object($conn))
$conn = self :: getDefaultConnection();
$obj = new $class_name(null, $conn);
return $obj->_findByIds($ids, $params);
}
/**
* Userland filter for findByIds() static method
* @see findByIds()
* @param array object ids
* @param mixed misc magic params
* @return iterator
*/
protected function _findByIds($ids, $params = array())
{
if(!is_array($ids) || !sizeof($ids))
return new lmbCollection();
else
{
$params['criteria'] = new lmbSQLFieldCriteria('id', $ids, lmbSQLFieldCriteria :: IN);
return self :: find(get_class($this), $params, $this->_db_conn);
}
}
/**
* Implements WACT template datasource component interface, this method simply calls find()
* @see find()
* @param mixed misc magic params
* @return iterator
*/
function getDataset($magic_params = array())
{
return self :: find(get_class($this), $magic_params, $this->_db_conn);
}
/**
* Finds a collection of objects in database using raw SQL
* @param string class name of the object
* @param string SQL
* @param object database connection object
* @return iterator
*/
static function findBySql($class_name, $sql, $conn = null)
{
if(!is_object($conn))
$conn = self :: getDefaultConnection();
$stmt = $conn->newStatement($sql);
return self :: decorateRecordSet($stmt->getRecordSet(), $class_name);
}
/**
* Finds first object in database using raw SQL
* @param string class name of the object
* @param string SQL
* @param object database connection object
* @return object
*/
static function findFirstBySql($class_name, $sql, $conn = null)
{
$rs = self :: findBySql($class_name, $sql, $conn);
$rs->paginate(0, 1);
$rs->rewind();
if($rs->valid())
return $rs->current();
}
/**
* Alias for findFirstBySql
* @see findFirstBySql()
* @return object
*/
static function findOneBySql($class_name, $sql, $conn = null)
{
return self :: findFirstBySql($class_name, $sql, $conn);
}
/**
* Generic objects finder.
* Using misc magic params it's possible to pass different search parameters.
* If passed as an array magic params can have the following properties:
* - criteria - apply specified criteria to collection can be a plain string or criteria object
* - limit,offset - apply limit,offset to collection
* - sort - sort collection by specified fields, e.g array('id' => 'desc', 'name' => 'asc')
* - first - return the first object of collection
* Some examples:
*
* //generic way to find a collection of objects using magic params,
* //in this case we want collection:
* // - to match 'name="hey"' criteria
* // - ordered by 'id' property using descendant sort
* // - limited to 3 items
* $books = self :: find('Book', array('criteria' => 'name="hey"',
* 'sort' => array('id' => 'desc'),
* 'limit' => 3));
* //returns a collection of all Book objects in database
* $books = self :: find('Book');
* //returns one object with specified id
* $books = self :: find('Book', 1);
* //returns a collection of objects which match plain text criteria
* $books = self :: find('Book', 'name="hey"');
* //returns a collection of objects which match criteria with placeholders
* $books = self :: find('Book', array('name=? and author=?', 'hey', 'bob'));
* //returns a collection of objects which match object criteria
* $books = self :: find('Book',
* new lmbSQLFieldCriteria('name', 'hey'));
*
* @param string class name of the object
* @param mixed misc magic params
* @param object database connection object
* @return iterator
*/
static function find($class_name, $magic_params = array(), $conn = null)
{
if(!is_object($conn))
$conn = self :: getDefaultConnection();
if(self :: _isCriteria($magic_params))
$params = array('criteria' => $magic_params);
elseif(is_int($magic_params))
return self :: findById($class_name, $magic_params, false, $conn);
elseif(!is_array($magic_params))
throw new lmbARException("Invalid magic params", array($magic_params));
else
$params = $magic_params;
if(!class_exists($class_name, true))
throw new lmbARException("Could not find class '$class_name'");
$obj = new $class_name(null, $conn);
return $obj->_find($params);
}
/**
* Userland filter for find() static method
* @see find()
* @param mixed misc magic params
* @return iterator
*/
protected function _find($params = array())
{
$criteria = isset($params['criteria']) ? $params['criteria'] : null;
$sort_params = isset($params['sort']) ? $params['sort'] : array();
$rs = $this->_decorateRecordSet($this->findAllRecords($criteria, $sort_params));
$return_first = false;
foreach(array_values($params) as $value)
{
if(is_string($value) && $value == 'first')
{
$return_first = true;
$params['limit'] = 1;
break;
}
}
if(isset($params['limit']))
$rs->paginate(isset($params['offset']) ? $params['offset'] : 0, $params['limit']);
if($return_first)
{
$rs->rewind();
if($rs->valid())
return $rs->current();
}
else
return $rs;
}
/**
* Finds a collection of records(not lmbActiveRecord objects!) from database table
* @param string|object filtering criteria
* @param array sort params
* @return iterator
*/
function findAllRecords($criteria = null, $sort_params = array())
{
if(!count($sort_params))
$sort_params = $this->_default_sort_params;
return $this->_db_table->select($this->addClassCriteria($criteria),
$sort_params,
$this->_getColumnsForSelect());
}
/**
* Adds class name criterion to passed in criteria
* @param string|object criteria
* @return object
*/
function addClassCriteria($criteria)
{
if($this->_isInheritable())
return lmbSQLCriteria :: objectify($criteria)->addAnd(array(self :: $_inheritance_field .
$this->getInheritanceCondition()));
return $criteria;
}
function getInheritanceCondition()
{
return ' LIKE "' . $this->_getInheritancePath() . '%"';
}
protected function _getInheritancePath()
{
$class = get_class($this);
$path = "$class|";
while($class = get_parent_class($class))
{
if($class == __CLASS__)
break;
$path = "$class|$path";
}
return $path;
}
static function decodeInheritancePath($path)
{
$items = explode('|', $path);
array_pop($items);//removing last empty item
return $items;
}
static function getInheritanceClass($obj)
{
return end(self :: decodeInheritancePath($obj[self :: $_inheritance_field]));
}
/**
* Loads current object with data from database, overwrites any previous data, marks object dirty and unsets new status
* @param integer object id
*/
function loadById($id)
{
$object = self :: findById(get_class($this), $id, true, $this->_db_conn);
$this->importRaw($object->exportRaw());
$this->_resetDirty();
$this->_is_new = false;
}
/**
* Loads current object with data from database record, overwrites any previous data, marks object dirty and unsets new status
* @param object database record object
*/
function loadFromRecord($record)
{
$decoded = $this->_decodeDbValues($record);
foreach($decoded as $key => $value)
$this->_setDbValue($key, $value);
$this->_resetDirty();
$this->_is_new = false;
return true;
}
protected function _setDbValue($key, $value)
{
if($this->_hasValueObjectRelation($key))
$this->raw_value_objects[$key] = $value;
else
parent :: _setRaw($key, $value);
}
protected function _decodeDbValues($record)
{
return $this->_db_meta_info->castDbValues($record);
}
/**
* Returns id of object typecasted to integer explicitly
* @return integer
*/
function getId()
{
if($id = $this->_getRaw('id'))
return (int)$id;
}
/**
* Sets id of an object typecasted to integer explicitly, be carefull using this method since
* it may break relations if used improperly
* @param integer
*/
function setId($id)
{
$this->_setRaw('id', (int)$id);
}
function getUpdateTime()
{
return $this->_getRaw(self :: $_utime_field);
}
function getCreateTime()
{
return $this->_getRaw(self :: $_ctime_field);
}
/**
* Destroys current object removing it from database as well, removes related objects if
* object was configured to do so. Throws exception if object doesn't have identity.
*/
function destroy()
{
if($this->_is_being_destroyed)
return;
if(!$this->getId())
throw new lmbARException('Id not set');
$this->_is_being_destroyed = true;
$this->_onBeforeDestroy();
$this->_invokeListeners(self :: ON_BEFORE_DESTROY);
$this->_removeOneToOneObjects();
$this->_removeOneToManyObjects();
$this->_removeManyToManyRecords();
$this->_removeBelongsToRelations();
$this->_deleteDbRecord();
$this->_onAfterDestroy();
$this->_invokeListeners(self :: ON_AFTER_DESTROY);
$this->_is_being_destroyed = false;
}
protected function _deleteDbRecord()
{
$this->_db_table->deleteById($this->getId());
}
/**
* Finds all objects which satisfy the passed criteria and destroys them one by one
* @param string class name
* @param string|object search criteria, if not set all objects are removed
* @param object database connection object
*/
static function delete($class_name, $criteria = null, $conn = null)
{
if(!is_object($conn))
$conn = self :: getDefaultConnection();
$params = array();
if($criteria)
$params = array('criteria' => $criteria);
$rs = self :: find($class_name, $params, $conn);
foreach($rs as $object)
$object->destroy();
}
static function deleteRaw($class_name, $criteria = null, $conn = null)
{
if(!is_object($conn))
$conn = self :: getDefaultConnection();
$object = new $class_name(null, $conn);
$db_table = $object->getDbTable();
$db_table->delete($criteria);
}
static function updateRaw($class_name, $set, $criteria = null, $conn = null)
{
if(!is_object($conn))
$conn = self :: getDefaultConnection();
$object = new $class_name(null, $conn);
$db_table = $object->getDbTable();
$db_table->update($set, $criteria);
}
protected function _getColumnsForSelect()
{
return $this->_db_table->getColumnsForSelect('', $this->_lazy_attributes);
}
protected function _removeOneToOneObjects()
{
foreach($this->_has_one as $property => $info)
{
if(isset($info['cascade_delete']) && !$info['cascade_delete'])
continue;
if($object = $this->get($property))
$object->destroy();
}
}
protected function _removeOneToManyObjects()
{
foreach($this->_has_many as $property => $info)
{
$collection = $this->get($property);
if(!$collection)
continue;
if(isset($info['nullify']) && $info['nullify'])
$collection->nullify();
else
$collection->removeAll();
}
}
protected function _removeManyToManyRecords()
{
foreach($this->_has_many_to_many as $property => $info)
{
if($collection = $this->get($property))
$collection->removeAll();
}
}
protected function _removeBelongsToRelations()
{
foreach($this->_belongs_to as $property => $info)
{
if($parent = $this->get($property))
{
$parent->set($info['field'], null);
$parent->save();
}
}
}
protected function _createSQLStatement($sql)
{
return $this->_db_conn->newStatement($sql);
}
protected function _query($sql)
{
$stmt = $this->_createSQLStatement($sql);
return $stmt->getRecordSet();
}
protected function _execute($sql)
{
$stmt = $this->_createSQLStatement($sql);
return $stmt->execute();
}
/**
* Decorates database recordset with special decorator which converts each record into
* corresponding lmbActiveRecord object.
* @see lmbARRecordSetDecorator
* @param iterator
* @param string wrapper class name
* @param object database connection object
*/
function decorateRecordSet($rs, $class, $conn = null)
{
if(!is_object($conn))
$conn = self :: getDefaultConnection();
return new lmbARRecordSetDecorator($rs, $class, $conn);
}
function _decorateRecordSet($rs)
{
return new lmbARRecordSetDecorator($rs, get_class($this), $this->_db_conn);
}
function __clone()
{
$this->remove('id');
}
/**
* Imports magically data into object using relation info. This method is magic because it allows to
* import scalar data into objects. E.g:
*
* //provided Book has Author many-to-one relation as 'author' property
* $book = new Book();
* //will try load Author with id = 2
* $book->import(array('title' => 'Alice in wonderand',
* 'author' => 2));
* //should print '2'
* echo $book->getAuthor()->getId();
*
* @param array|object
*/
function import($source)
{
if(is_object($source))
{
if($source instanceof lmbActiveRecord)
{
$this->importRaw($source->exportRaw());
$this->setIsNew($source->isNew());
}
else
$this->import($source->export());
return;
}
foreach($source as $property => $value)
{
if(isset($this->_composed_of[$property]))
$this->_importValueObject($property, $value);
elseif(isset($this->_has_many[$property]))
$this->_importCollection($property, $value, $this->_has_many[$property]['class']);
elseif(isset($this->_has_many_to_many[$property]))
$this->_importCollection($property, $value, $this->_has_many_to_many[$property]['class']);
elseif(isset($this->_belongs_to[$property]))
$this->_importEntity($property, $value, $this->_belongs_to[$property]['class']);
elseif(isset($this->_many_belongs_to[$property]))
$this->_importEntity($property, $value, $this->_many_belongs_to[$property]['class']);
elseif(isset($this->_has_one[$property]))
$this->_importEntity($property, $value, $this->_has_one[$property]['class']);
elseif($this->_canImportProperty($property))
$this->set($property, $value);
}
}
/**
* Plain import of data into object
* @see lmbObject::import()
* @param array
*/
function importRaw($source)
{
parent :: import($source);
}
protected function _canImportProperty($property)
{
if($this->isNew())
return true;
if($property == 'id')
return false;
return true;
}
protected function _importCollection($property, $value, $class)
{
if(is_array($value))
{
$objects = array();
foreach($value as $item)
{
if(is_numeric($item))
$objects[] = new $class((int)$item, $this->_db_conn);
elseif(is_object($item))
$objects[] = $item;
}
$this->get($property)->set($objects);
}
}
protected function _importEntity($property, $value, $class)
{
if(is_numeric($value))
{
$obj = new $class((int)$value, $this->_db_conn);
$this->set($property, $obj);
}
elseif(is_object($value))
$this->set($property, $value);
elseif(is_null($value) || strcasecmp($value, 'null') === 0 || ($value === ''))
$this->set($property, null);
}
protected function _importValueObject($property, $obj)
{
if(!is_object($obj))
$this->set($property, $this->_createValueObject($this->_composed_of[$property]['class'], $obj));
else
$this->set($property, $obj);
}
/**
* Exports object data with lazy properties resolved
* @return array
*/
function export()
{
if(!$this->isNew() && $this->_hasLazyAttributes())
$this->_loadLazyAttributes();
return parent :: export();
}
/**
* Plain export of object data(lazy properties not included if not loaded)
* @see lmbObject::export()
* @return array
*/
function exportRaw()
{
return parent :: export();
}
/**
* Registers instance listener of specified type
* @param integer call back type
* @param object call back object
*/
function registerCallback($type, $callback)
{
$this->_listeners[$type][] = lmbDelegate :: objectify($callback);
}
function registerOnBeforeSaveCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_BEFORE_SAVE, $args);
}
function registerOnAfterSaveCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_AFTER_SAVE, $args);
}
function registerOnBeforeUpdateCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_BEFORE_UPDATE, $args);
}
function registerOnUpdateCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_UPDATE, $args);
}
function registerOnAfterUpdateCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_AFTER_UPDATE, $args);
}
function registerOnBeforeCreateCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_BEFORE_CREATE, $args);
}
function registerOnCreateCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_CREATE, $args);
}
function registerOnAfterCreateCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_AFTER_CREATE, $args);
}
function registerOnBeforeDestroyCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_BEFORE_DESTROY, $args);
}
function registerOnAfterDestroyCallback($callback)
{
$args = func_get_args();
$this->registerCallback(self :: ON_AFTER_DESTROY, $args);
}
/**
* Registers global listener of specified type
* @param integer call back type
* @param object call back object
*/
static function registerGlobalCallback($type, $callback)
{
self :: $_global_listeners[$type][] = lmbDelegate :: objectify($callback);
}
function registerGlobalOnBeforeSaveCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_BEFORE_SAVE, $args);
}
function registerGlobalOnAfterSaveCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_AFTER_SAVE, $args);
}
function registerGlobalOnBeforeUpdateCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_BEFORE_UPDATE, $args);
}
function registerGlobalOnUpdateCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_UPDATE, $args);
}
function registerGlobalOnAfterUpdateCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_AFTER_UPDATE, $args);
}
function registerGlobalOnBeforeCreateCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_BEFORE_CREATE, $args);
}
function registerGlobalOnCreateCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_CREATE, $args);
}
function registerGlobalOnAfterCreateCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_AFTER_CREATE, $args);
}
function registerGlobalOnBeforeDestroyCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_BEFORE_DESTROY, $args);
}
function registerGlobalOnAfterDestroyCallback($callback)
{
$args = func_get_args();
self :: registerGlobalCallback(self :: ON_AFTER_DESTROY, $args);
}
protected function _invokeListeners($type)
{
if(isset($this->_listeners[$type]))
lmbDelegate :: invokeAll($this->_listeners[$type], array($this));
if(isset(self :: $_global_listeners[$type]))
lmbDelegate :: invokeAll(self :: $_global_listeners[$type], array($this));
}
}
?>