[limb-svn] r6316 - in 3.x/trunk/limb/active_record: src tests/cases

svn at limb-project.com svn at limb-project.com
Wed Sep 19 21:51:10 MSD 2007


Author: pachanga
Date: 2007-09-19 21:51:10 +0400 (Wed, 19 Sep 2007)
New Revision: 6316
URL: http://fisheye.limb-project.com/changelog/limb/?cs=6316

Modified:
   3.x/trunk/limb/active_record/src/lmbActiveRecord.class.php
   3.x/trunk/limb/active_record/tests/cases/lmbActiveRecordTest.class.php
Log:
-- adding experimental support for short alternatives of static update*,delete* calls

Modified: 3.x/trunk/limb/active_record/src/lmbActiveRecord.class.php
===================================================================
--- 3.x/trunk/limb/active_record/src/lmbActiveRecord.class.php	2007-09-19 11:35:40 UTC (rev 6315)
+++ 3.x/trunk/limb/active_record/src/lmbActiveRecord.class.php	2007-09-19 17:51:10 UTC (rev 6316)
@@ -1,2043 +1,2074 @@
-<?php
-/*
- * Limb PHP Framework
- *
- * @link http://limb-project.com
- * @copyright  Copyright &copy; 2004-2007 BIT(http://bit-creative.com)
- * @license    LGPL http://www.gnu.org/copyleft/lesser.html
- */
-lmb_require('limb/core/src/lmbObject.class.php');
-lmb_require('limb/core/src/lmbDelegate.class.php');
-lmb_require('limb/core/src/lmbCollection.class.php');
-lmb_require('limb/dbal/src/lmbTableGateway.class.php');
-lmb_require('limb/dbal/src/criteria/lmbSQLCriteria.class.php');
-lmb_require('limb/dbal/src/drivers/lmbDbTypeInfo.class.php');
-lmb_require('limb/dbal/toolkit.inc.php');
-lmb_require('limb/validation/src/lmbValidator.class.php');
-lmb_require('limb/validation/src/lmbErrorList.class.php');
-lmb_require('limb/validation/src/exception/lmbValidationException.class.php');
-lmb_require('limb/active_record/src/lmbARException.class.php');
-lmb_require('limb/active_record/src/lmbARNotFoundException.class.php');
-lmb_require('limb/active_record/src/lmbARRecordSetDecorator.class.php');
-lmb_require('limb/active_record/src/lmbAROneToManyCollection.class.php');
-lmb_require('limb/active_record/src/lmbARManyToManyCollection.class.php');
-
-/**
- * Base class responsible for ActiveRecord design pattern implementation. Inspired by Rails ActiveRecord class.
- *
- * @version $Id$
- * @package active_record
- */
-class lmbActiveRecord extends lmbObject
-{
-  /**
-   * @var string database column name used to store object class name for single table inheritance
-   */
-  protected static $_inheritance_field = 'kind';
-  /**
-   * @var string database column name used to store object's create time
-   */
-  protected static $_ctime_field = 'ctime';
-  /**
-   * @var string database column name used to store object's update time
-   */
-  protected static $_utime_field = 'utime';
-  /**
-   * @var array global event listeners which receieve events from ALL lmbActiveRecord instances
-   */
-  protected static $_global_listeners = array();
-  /**
-   * @var object database connection which is shared by all lmbActiveRecord instances
-   *             if no connection passed explicitly into constructor
-   */
-  protected static $_default_db_conn;
-  /**
-   * @var object current object's database connection
-   *  @see lmbDbConnection
-   */
-  protected $_db_conn;
-  /**
-   * @var object lmbTableGateway instance used to access underlying db table
-   */
-  protected $_db_table;
-  /**
-   * @var string name of class database table to store instance fields, if not set lmbActiveRecord tries to guess it
-   */
-  protected $_db_table_name;
-  /**
-   * @var boolean reflects new or loaded status of an object
-   */
-  protected $_is_new = true;
-  /**
-   * @var object error list instance used to store validation errors
-   */
-  protected $_error_list;
-  /**
-   * @var array all has-one relations of an object
-   */
-  protected $_has_one = array();
-  /**
-   * @var array all belongs-to relations of an object
-   */
-  protected $_belongs_to = array();
-  /**
-   * @var array all many-belongs-to relations of an object
-   */
-  protected $_many_belongs_to = array();
-  /**
-   * @var array all has-many relations of an object
-   */
-  protected $_has_many = array();
-  /**
-   * @var array all has-many-to-many relations of an object
-   */
-  protected $_has_many_to_many = array();
-  /**
-   * @var array all value object relations of an object
-   */
-  protected $_composed_of = array();
-  /**
-   * @var boolean true during the object's saving procedure
-   */
-  protected $_is_being_saved = false;
-  /**
-   * @var boolean true during the object's removal procedure
-   */
-  protected $_is_being_destroyed = false;
-  /**
-   * @var boolean object's dirtiness status
-   */
-  protected $_is_dirty = false;
-  /**
-   * @var boolean we can explicitly mark object inheritable or not, if not set lmbActiveRecord looks if inheritance field is present in db
-   */
-  protected $_is_inheritable;
-  /**
-   * @var array array of attributes which should not be loaded at once but only on demand
-   */
-  protected $_lazy_attributes = array();
-  /**
-   * @var array array of dirty(changed) attributes of an object
-   */
-  protected $_dirty_props = array();
-  /**
-   * @var array sort params used to order objects during database retrieval
-   */
-  protected $_default_sort_params;
-  /**
-   * @var object database metainfo object
-   */
-  protected $_db_meta_info;
-
-  /**#@+
-   * Event type constants
-   */
-  const ON_BEFORE_SAVE             = 1;
-  const ON_AFTER_SAVE              = 2;
-  const ON_BEFORE_UPDATE           = 3;
-  const ON_UPDATE                  = 4;
-  const ON_AFTER_UPDATE            = 5;
-  const ON_BEFORE_CREATE           = 6;
-  const ON_CREATE                  = 7;
-  const ON_AFTER_CREATE            = 8;
-  const ON_BEFORE_DESTROY          = 9;
-  const ON_AFTER_DESTROY           = 10;
-  /**#@-*/
-
-  /**
-   * @var array event listeners attached to the concrete object instance
-   */
-  protected $_listeners = array();
-
-  /**
-   * Note, this property is not guarded with "_" prefix since we need it to be imported/exported
-   * @var array An array of attached value objects
-   */
-  protected $raw_value_objects = array();
-
-  /**
-   *  Constructor.
-   *  Creates an instance of lmbActiveRecord object in different ways depending on passed argument
-   *  <code>
-   *  //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);
-   *  </code>
-   *  @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;
-  }
-
-  function setErrorList($error_list)
-  {
-    $this->_error_list = $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:
-   *
-   *  <code>
-   *  $validator = new lmbValidator();
-   *  $validator->addRequiredRule('title');
-   *  return $validator;
-   *  </code>
-   *  @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 _onAfterImport(){}
-
-  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)
-      {
-        //remove obsolete check for 'first' property
-        if(!is_int($key) || $value == 'first')
-          return false;
-      }
-      return true;
-    }
-    return false;
-  }
-
-  protected static function _isClass($name)
-  {
-    if(!is_scalar($name) || !$name)
-      return false;
-
-    return ctype_alnum("$name") && !ctype_digit("$name");
-  }
-
-  protected function _getCallingClass($method, $at = 1)
-  {
-    //once PHP-5.3 LSB patch is available use a better alternative
-    //currently it's a quite a slow implementation and it doesn't 
-    //recognize multiline function calls
-
-    $trace = debug_backtrace();   
-    $back = $trace[$at];  
-    $fp = fopen($back['file'], 'r');
-
-    for($i=0; $i<$back['line']-1; $i++)
-      fgets($fp);
-
-    $line = fgets($fp); 
-    fclose($fp);
-
-    if(!preg_match('~(\w+)\s*::\s*' . $method . '\s*\(~', $line, $m))
-      throw new lmbARException("Static calling class not found!");
-    if($m[1] == 'lmbActiveRecord')
-      throw new lmbARException("Found static class can't be lmbActiveRecord!");
-    return $m[1]; 
-  }
-
-  /**
-   *  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 = null, $magic_params = null, $conn = null)
-  {
-    if(!self :: _isClass($class_name))
-    {
-      $conn = $magic_params;
-      $magic_params = $class_name ? $class_name : array();
-      $class_name = self :: _getCallingClass(__FUNCTION__);
-    }
-
-    $params = array();
-    if(self :: _isCriteria($magic_params))
-      $params = array('first', 'criteria' => $magic_params);
-    elseif(is_null($magic_params))
-      $params = array('first');
-    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 = null, $magic_params = null, $conn = null)
-  {
-    if(!self :: _isClass($class_name))
-    {
-      $conn = $magic_params;
-      $magic_params = $class_name ? $class_name : array();
-      $class_name = self :: _getCallingClass(__FUNCTION__);
-    }
-    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 = null, $params = null, $conn = null)
-  {
-    if(!self :: _isClass($class_name))
-    {
-      $conn = $params;
-      $params = $ids;
-      $ids = $class_name;
-      $class_name = self :: _getCallingClass(__FUNCTION__);
-    }
-
-    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 = null, $conn = null)
-  {
-    if(!self :: _isClass($class_name))
-    {
-      $conn = $sql;
-      $sql = $class_name;
-      $class_name = self :: _getCallingClass(__FUNCTION__);
-    }
-
-    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 = null, $conn = null)
-  {
-    if(!self :: _isClass($class_name))
-    {
-      $conn = $sql;
-      $sql = $class_name;
-      $class_name = self :: _getCallingClass(__FUNCTION__);
-    }
-
-    $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 = null, $conn = null)
-  {
-    if(!self :: _isClass($class_name))
-    {
-      $conn = $sql;
-      $sql = $class_name;
-      $class_name = self :: _getCallingClass(__FUNCTION__);
-    }
-    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:
-   *   - <b>criteria</b> - apply specified criteria to collection can be a plain string or criteria object
-   *   - <b>limit,offset</b> - apply limit,offset to collection
-   *   - <b>sort</b>  - sort collection by specified fields, e.g array('id' => 'desc', 'name' => 'asc')
-   *   - <b>first</b> - return the first object of collection
-   *  Some examples:
-   *  <code>
-   *  //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'));
-   *  </code>
-   *  @param string class name of the object
-   *  @param mixed misc magic params
-   *  @param object database connection object
-   *  @return iterator
-   */
-  static function find($class_name = null, $magic_params = null, $conn = null)
-  {
-    if(!self :: _isClass($class_name))
-    {
-      $conn = $magic_params;
-      $magic_params = $class_name ? $class_name : array();
-      $class_name = self :: _getCallingClass(__FUNCTION__);
-    }
-
-    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_null($magic_params))
-      $params = array();
-    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;
-  }
-
-  function remove($name)
-  {
-    parent :: remove($name);
-
-    if(isset($this->raw_value_objects[$name]))
-      unset($this->raw_value_objects[$name]);
-  }
-
-  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:
-   *  <code>
-   *  //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();
-   *  </code>
-   *  @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);
-    }
-    $this->_onAfterImport();
-  }
-  /**
-   *  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))
-    {
-      $class = $this->_composed_of[$property]['class'];
-      $this->set($property, $this->_createValueObject($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));
-  }
-}
-
-
+<?php
+/*
+ * Limb PHP Framework
+ *
+ * @link http://limb-project.com
+ * @copyright  Copyright &copy; 2004-2007 BIT(http://bit-creative.com)
+ * @license    LGPL http://www.gnu.org/copyleft/lesser.html
+ */
+lmb_require('limb/core/src/lmbObject.class.php');
+lmb_require('limb/core/src/lmbDelegate.class.php');
+lmb_require('limb/core/src/lmbCollection.class.php');
+lmb_require('limb/dbal/src/lmbTableGateway.class.php');
+lmb_require('limb/dbal/src/criteria/lmbSQLCriteria.class.php');
+lmb_require('limb/dbal/src/drivers/lmbDbTypeInfo.class.php');
+lmb_require('limb/dbal/toolkit.inc.php');
+lmb_require('limb/validation/src/lmbValidator.class.php');
+lmb_require('limb/validation/src/lmbErrorList.class.php');
+lmb_require('limb/validation/src/exception/lmbValidationException.class.php');
+lmb_require('limb/active_record/src/lmbARException.class.php');
+lmb_require('limb/active_record/src/lmbARNotFoundException.class.php');
+lmb_require('limb/active_record/src/lmbARRecordSetDecorator.class.php');
+lmb_require('limb/active_record/src/lmbAROneToManyCollection.class.php');
+lmb_require('limb/active_record/src/lmbARManyToManyCollection.class.php');
+
+/**
+ * Base class responsible for ActiveRecord design pattern implementation. Inspired by Rails ActiveRecord class.
+ *
+ * @version $Id$
+ * @package active_record
+ */
+class lmbActiveRecord extends lmbObject
+{
+  /**
+   * @var string database column name used to store object class name for single table inheritance
+   */
+  protected static $_inheritance_field = 'kind';
+  /**
+   * @var string database column name used to store object's create time
+   */
+  protected static $_ctime_field = 'ctime';
+  /**
+   * @var string database column name used to store object's update time
+   */
+  protected static $_utime_field = 'utime';
+  /**
+   * @var array global event listeners which receieve events from ALL lmbActiveRecord instances
+   */
+  protected static $_global_listeners = array();
+  /**
+   * @var object database connection which is shared by all lmbActiveRecord instances
+   *             if no connection passed explicitly into constructor
+   */
+  protected static $_default_db_conn;
+  /**
+   * @var object current object's database connection
+   *  @see lmbDbConnection
+   */
+  protected $_db_conn;
+  /**
+   * @var object lmbTableGateway instance used to access underlying db table
+   */
+  protected $_db_table;
+  /**
+   * @var string name of class database table to store instance fields, if not set lmbActiveRecord tries to guess it
+   */
+  protected $_db_table_name;
+  /**
+   * @var boolean reflects new or loaded status of an object
+   */
+  protected $_is_new = true;
+  /**
+   * @var object error list instance used to store validation errors
+   */
+  protected $_error_list;
+  /**
+   * @var array all has-one relations of an object
+   */
+  protected $_has_one = array();
+  /**
+   * @var array all belongs-to relations of an object
+   */
+  protected $_belongs_to = array();
+  /**
+   * @var array all many-belongs-to relations of an object
+   */
+  protected $_many_belongs_to = array();
+  /**
+   * @var array all has-many relations of an object
+   */
+  protected $_has_many = array();
+  /**
+   * @var array all has-many-to-many relations of an object
+   */
+  protected $_has_many_to_many = array();
+  /**
+   * @var array all value object relations of an object
+   */
+  protected $_composed_of = array();
+  /**
+   * @var boolean true during the object's saving procedure
+   */
+  protected $_is_being_saved = false;
+  /**
+   * @var boolean true during the object's removal procedure
+   */
+  protected $_is_being_destroyed = false;
+  /**
+   * @var boolean object's dirtiness status
+   */
+  protected $_is_dirty = false;
+  /**
+   * @var boolean we can explicitly mark object inheritable or not, if not set lmbActiveRecord looks if inheritance field is present in db
+   */
+  protected $_is_inheritable;
+  /**
+   * @var array array of attributes which should not be loaded at once but only on demand
+   */
+  protected $_lazy_attributes = array();
+  /**
+   * @var array array of dirty(changed) attributes of an object
+   */
+  protected $_dirty_props = array();
+  /**
+   * @var array sort params used to order objects during database retrieval
+   */
+  protected $_default_sort_params;
+  /**
+   * @var object database metainfo object
+   */
+  protected $_db_meta_info;
+
+  /**#@+
+   * Event type constants
+   */
+  const ON_BEFORE_SAVE             = 1;
+  const ON_AFTER_SAVE              = 2;
+  const ON_BEFORE_UPDATE           = 3;
+  const ON_UPDATE                  = 4;
+  const ON_AFTER_UPDATE            = 5;
+  const ON_BEFORE_CREATE           = 6;
+  const ON_CREATE                  = 7;
+  const ON_AFTER_CREATE            = 8;
+  const ON_BEFORE_DESTROY          = 9;
+  const ON_AFTER_DESTROY           = 10;
+  /**#@-*/
+
+  /**
+   * @var array event listeners attached to the concrete object instance
+   */
+  protected $_listeners = array();
+
+  /**
+   * Note, this property is not guarded with "_" prefix since we need it to be imported/exported
+   * @var array An array of attached value objects
+   */
+  protected $raw_value_objects = array();
+
+  /**
+   *  Constructor.
+   *  Creates an instance of lmbActiveRecord object in different ways depending on passed argument
+   *  <code>
+   *  //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);
+   *  </code>
+   *  @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;
+  }
+
+  function setErrorList($error_list)
+  {
+    $this->_error_list = $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:
+   *
+   *  <code>
+   *  $validator = new lmbValidator();
+   *  $validator->addRequiredRule('title');
+   *  return $validator;
+   *  </code>
+   *  @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 _onAfterImport(){}
+
+  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)
+      {
+        //remove obsolete check for 'first' property
+        if(!is_int($key) || $value == 'first')
+          return false;
+      }
+      return true;
+    }
+    return false;
+  }
+
+  protected static function _isClass($name)
+  {
+    if(!is_scalar($name) || !$name)
+      return false;
+
+    return ctype_alnum("$name") && !ctype_digit("$name");
+  }
+
+  protected function _getCallingClass()
+  {
+    //once PHP-5.3 LSB patch is available we'll use get_called_class
+    //currently it's a quite a slow implementation and it doesn't 
+    //recognize multiline function calls
+
+    $trace = debug_backtrace();   
+    $back = $trace[1];  
+    $method = $back['function'];
+    $fp = fopen($back['file'], 'r');
+
+    for($i=0; $i<$back['line']-1; $i++)
+      fgets($fp);
+
+    $line = fgets($fp); 
+    fclose($fp);
+
+    if(!preg_match('~(\w+)\s*::\s*' . $method . '\s*\(~', $line, $m))
+      throw new lmbARException("Static calling class not found!(using multiline static method call?)");
+    if($m[1] == 'lmbActiveRecord')
+      throw new lmbARException("Found static class can't be lmbActiveRecord!");
+    return $m[1]; 
+  }
+
+  /**
+   *  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 = null, $magic_params = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $magic_params;
+      $magic_params = $class_name ? $class_name : array();
+      $class_name = self :: _getCallingClass();
+    }
+
+    $params = array();
+    if(self :: _isCriteria($magic_params))
+      $params = array('first', 'criteria' => $magic_params);
+    elseif(is_null($magic_params))
+      $params = array('first');
+    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 = null, $magic_params = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $magic_params;
+      $magic_params = $class_name ? $class_name : array();
+      $class_name = self :: _getCallingClass();
+    }
+    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 = null, $throw_exception = true, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $throw_exception;
+      $throw_exception = $id;
+      $id = $class_name;
+      $class_name = self :: _getCallingClass();
+    }
+
+    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 = null, $params = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $params;
+      $params = $ids;
+      $ids = $class_name;
+      $class_name = self :: _getCallingClass();
+    }
+
+    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 = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $sql;
+      $sql = $class_name;
+      $class_name = self :: _getCallingClass();
+    }
+
+    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 = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $sql;
+      $sql = $class_name;
+      $class_name = self :: _getCallingClass();
+    }
+
+    $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 = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $sql;
+      $sql = $class_name;
+      $class_name = self :: _getCallingClass();
+    }
+    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:
+   *   - <b>criteria</b> - apply specified criteria to collection can be a plain string or criteria object
+   *   - <b>limit,offset</b> - apply limit,offset to collection
+   *   - <b>sort</b>  - sort collection by specified fields, e.g array('id' => 'desc', 'name' => 'asc')
+   *   - <b>first</b> - return the first object of collection
+   *  Some examples:
+   *  <code>
+   *  //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'));
+   *  </code>
+   *  @param string class name of the object
+   *  @param mixed misc magic params
+   *  @param object database connection object
+   *  @return iterator
+   */
+  static function find($class_name = null, $magic_params = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $magic_params;
+      $magic_params = $class_name ? $class_name : array();
+      $class_name = self :: _getCallingClass();
+    }
+
+    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_null($magic_params))
+      $params = array();
+    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;
+  }
+
+  function remove($name)
+  {
+    parent :: remove($name);
+
+    if(isset($this->raw_value_objects[$name]))
+      unset($this->raw_value_objects[$name]);
+  }
+
+  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 = null, $criteria = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $criteria;
+      $criteria = $class_name;
+      $class_name = self :: _getCallingClass();
+    }
+
+    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 = null, $criteria = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $criteria;
+      $criteria = $class_name;
+      $class_name = self :: _getCallingClass();
+    }
+
+    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 = null, $criteria = null, $conn = null)
+  {
+    if(!self :: _isClass($class_name))
+    {
+      $conn = $criteria;
+      $criteria = $set;
+      $set = $class_name;
+      $class_name = self :: _getCallingClass();
+    }
+
+    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:
+   *  <code>
+   *  //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();
+   *  </code>
+   *  @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);
+    }
+    $this->_onAfterImport();
+  }
+  /**
+   *  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))
+    {
+      $class = $this->_composed_of[$property]['class'];
+      $this->set($property, $this->_createValueObject($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));
+  }
+}
+
+

Modified: 3.x/trunk/limb/active_record/tests/cases/lmbActiveRecordTest.class.php
===================================================================
--- 3.x/trunk/limb/active_record/tests/cases/lmbActiveRecordTest.class.php	2007-09-19 11:35:40 UTC (rev 6315)
+++ 3.x/trunk/limb/active_record/tests/cases/lmbActiveRecordTest.class.php	2007-09-19 17:51:10 UTC (rev 6316)
@@ -1,870 +1,997 @@
-<?php
-/*
- * Limb PHP Framework
- *
- * @link http://limb-project.com
- * @copyright  Copyright &copy; 2004-2007 BIT(http://bit-creative.com)
- * @license    LGPL http://www.gnu.org/copyleft/lesser.html
- */
-require_once('limb/active_record/src/lmbActiveRecord.class.php');
-require_once('limb/dbal/src/criteria/lmbSQLRawCriteria.class.php');
-require_once('limb/dbal/src/lmbSimpleDb.class.php');
-require_once('limb/dbal/src/lmbTableGateway.class.php');
-
-class TestOneTableObject extends lmbActiveRecord
-{
-  protected $_db_table_name = 'test_one_table_object';
-  protected $dummy;
-}
-
-class TestOneTableObjectFailing extends lmbActiveRecord
-{
-  var $fail;
-  protected $_db_table_name = 'test_one_table_object';
-
-  protected function _onAfterSave()
-  {
-    if(is_object($this->fail))
-      throw $this->fail;
-  }
-}
-
-class TestOneTableObjectWithCustomDestroy extends lmbActiveRecord
-{
-  protected $_db_table_name = 'test_one_table_object';
-
-  function destroy()
-  {
-    parent :: destroy();
-    echo "destroyed!";
-  }
-}
-
-class TestOneTableObjectWithHooks extends TestOneTableObject
-{
-  protected function _onValidate()
-  {
-    echo '|on_validate|';
-  }
-
-  protected function _onBeforeUpdate()
-  {
-    echo '|on_before_update|';
-  }
-
-  protected function _onBeforeCreate()
-  {
-    echo '|on_before_create|';
-  }
-
-  protected function _onBeforeSave()
-  {
-    echo '|on_before_save|';
-  }
-
-  protected function _onAfterSave()
-  {
-    echo '|on_after_save|';
-  }
-
-  protected function _onSave()
-  {
-    echo '|on_save|';
-  }
-
-  protected function _onUpdate()
-  {
-    echo '|on_update|';
-  }
-
-  protected function _onCreate()
-  {
-    echo '|on_create|';
-  }
-
-  protected function _onAfterUpdate()
-  {
-    echo '|on_after_update|';
-  }
-
-  protected function _onAfterCreate()
-  {
-    echo '|on_after_create|';
-  }
-
-  protected function _onBeforeDestroy()
-  {
-    echo '|on_before_destroy|';
-  }
-
-  protected function _onAfterDestroy()
-  {
-    echo '|on_after_destroy|';
-  }
-}
-
-class TestOneTableObjectWithSortParams extends TestOneTableObject
-{
-  protected $_default_sort_params = array('id' => 'DESC');
-}
-
-class lmbActiveRecordTest extends UnitTestCase
-{
-  var $conn;
-  var $db;
-  var $class_name = 'TestOneTableObject';
-
-  function setUp()
-  {
-    $toolkit = lmbToolkit :: save();
-    $this->conn = $toolkit->getDefaultDbConnection();
-    $this->db = new lmbSimpleDb($this->conn);
-
-    $this->_cleanUp();
-  }
-
-  function tearDown()
-  {
-    $this->_cleanUp();
-
-    lmbToolkit :: restore();
-  }
-
-  function _cleanUp()
-  {
-    $this->db->delete('test_one_table_object');
-  }
-
-  function testSaveNewRecord()
-  {
-    $object = new TestOneTableObject();
-    $object->set('annotation', $annotation = 'Super annotation');
-    $object->set('content', $content = 'Super content');
-    $object->set('news_date', $news_date = '2005-01-10');
-
-    $this->assertTrue($object->isNew());
-
-    $id = $object->save();
-
-    $this->assertFalse($object->isNew());
-    $this->assertNotNull($object->getId());
-    $this->assertEqual($object->getId(), $id);
-
-    $this->assertEqual($this->db->count('test_one_table_object'), 1);
-
-    $record = $this->db->selectRecord('test_one_table_object');
-    $this->assertEqual($record->get('id'), $id);
-    $this->assertEqual($record->get('annotation'), $annotation);
-    $this->assertEqual($record->get('content'), $content);
-    $this->assertEqual($record->get('news_date'), $news_date);
-    $this->assertEqual($record->get('id'), $object->getId());
-  }
-
-  function testDontCreateNewRecordTwice()
-  {
-    $object = $this->_initActiveRecordWithData(new TestOneTableObject());
-
-    $object->save();
-    $object->save();
-
-    $this->assertTrue($object->getId());
-
-    $this->assertEqual($this->db->count('test_one_table_object'), 1);
-  }
-
-  function testIsNew()
-  {
-    $object = $this->_initActiveRecordWithData(new TestOneTableObject());
-    $this->assertTrue($object->isNew());
-
-    $object->save();
-    $this->assertFalse($object->isNew());
-
-    $object->setIsNew();
-
-    $this->assertTrue($object->isNew());
-  }
-
-  function testDetach()
-  {
-    $object = $this->_initActiveRecordWithData(new TestOneTableObject());
-    $this->assertTrue($object->isNew());
-
-    $object->save();
-    $this->assertFalse($object->isNew());
-    $this->assertNotNull($object->getId());
-
-    $object->detach();
-
-    $this->assertTrue($object->isNew());
-    $this->assertNull($object->getId());
-
-    $object->save();
-
-    $this->assertEqual($this->db->count('test_one_table_object'), 2);
-  }
-
-  function testSaveUpdate()
-  {
-    $object = $this->_initActiveRecordWithDataAndSave(new TestOneTableObject());
-
-    $object->set('annotation', $annotation = 'Other annotation');
-    $object->set('content', $content = 'Other content');
-    $object->set('news_date', $news_date = '2005-10-20');
-    $object->save();
-
-    $this->assertEqual($this->db->count('test_one_table_object'), 1);
-
-    $record = $this->db->selectRecord('test_one_table_object');
-
-    $this->assertEqual($record->get('annotation'), $annotation);
-    $this->assertEqual($record->get('content'), $content);
-    $this->assertEqual($record->get('news_date'), $news_date);
-    $this->assertEqual($record->get('id'), $object->getId());
-  }
-
-  function testProperOrderOfCreateHooksCalls()
-  {
-    $object = new TestOneTableObjectWithHooks();
-    $object->setContent('whatever');
-
-    ob_start();
-    $object->save();
-    $str = ob_get_contents();
-    ob_end_clean();
-    $this->assertEqual($str, '|on_before_save||on_before_create||on_validate||on_save||on_create||on_after_create||on_after_save|');
-  }
-
-  function testProperOrderOfUpdateHooksCalls()
-  {
-    $object = new TestOneTableObjectWithHooks();
-    $object->setContent('whatever');
-    ob_start();
-    $object->save();
-    ob_end_clean();
-
-    $object->setContent('other content');
-
-    ob_start();
-    $object->save();
-    $str = ob_get_contents();
-    ob_end_clean();
-    $this->assertEqual($str, '|on_before_save||on_before_update||on_validate||on_save||on_update||on_after_update||on_after_save|');
-  }
-
-  function testProperOrderOfDestroyHooksCalls()
-  {
-    $object = new TestOneTableObjectWithHooks();
-    $object->setContent('whatever');
-    ob_start();
-    $object->save();
-    ob_clean();
-
-    $object->destroy();
-    $str = ob_get_content