summaryrefslogtreecommitdiff
path: root/lib/prado/framework/Data/ActiveRecord
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2016-02-24 23:18:07 +0100
committeremkael <emkael@tlen.pl>2016-02-24 23:18:07 +0100
commit6f7fdef0f500cd4bb540affd3bc1482243f337c1 (patch)
tree4853eecd0769a903e6130c1896e1d070848150dd /lib/prado/framework/Data/ActiveRecord
parent61f2ea48a4e11cb5fb941b3783e19c9e9ef38a45 (diff)
* Prado 3.3.0
Diffstat (limited to 'lib/prado/framework/Data/ActiveRecord')
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Exceptions/TActiveRecordException.php46
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Exceptions/messages.txt25
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php138
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php121
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php376
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php145
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php254
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php230
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TIbmScaffoldInput.php51
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TMssqlScaffoldInput.php53
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TMysqlScaffoldInput.php83
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TPgsqlScaffoldInput.php54
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TScaffoldInputBase.php103
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TScaffoldInputCommon.php309
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TSqliteScaffoldInput.php99
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldBase.php205
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldEditView.php306
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldEditView.tpl21
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldListView.php304
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldListView.tpl57
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldSearch.php150
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldSearch.tpl4
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldView.php141
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldView.tpl11
-rw-r--r--lib/prado/framework/Data/ActiveRecord/Scaffold/style.css124
-rw-r--r--lib/prado/framework/Data/ActiveRecord/TActiveRecord.php1107
-rw-r--r--lib/prado/framework/Data/ActiveRecord/TActiveRecordConfig.php199
-rw-r--r--lib/prado/framework/Data/ActiveRecord/TActiveRecordCriteria.php37
-rw-r--r--lib/prado/framework/Data/ActiveRecord/TActiveRecordGateway.php423
-rw-r--r--lib/prado/framework/Data/ActiveRecord/TActiveRecordManager.php162
30 files changed, 5338 insertions, 0 deletions
diff --git a/lib/prado/framework/Data/ActiveRecord/Exceptions/TActiveRecordException.php b/lib/prado/framework/Data/ActiveRecord/Exceptions/TActiveRecordException.php
new file mode 100644
index 0000000..c4b8adf
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Exceptions/TActiveRecordException.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * TActiveRecordException class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord
+ */
+
+/**
+ * Base exception class for Active Records.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord
+ * @since 3.1
+ */
+class TActiveRecordException extends TDbException
+{
+ /**
+ * @return string path to the error message file
+ */
+ protected function getErrorMessageFile()
+ {
+ $lang=Prado::getPreferredLanguage();
+ $path = dirname(__FILE__);
+ $msgFile=$path.'/messages-'.$lang.'.txt';
+ if(!is_file($msgFile))
+ $msgFile=$path.'/messages.txt';
+ return $msgFile;
+ }
+}
+
+/**
+ * TActiveRecordConfigurationException class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord
+ * @since 3.1
+ */
+class TActiveRecordConfigurationException extends TActiveRecordException
+{
+
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Exceptions/messages.txt b/lib/prado/framework/Data/ActiveRecord/Exceptions/messages.txt
new file mode 100644
index 0000000..0702c84
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Exceptions/messages.txt
@@ -0,0 +1,25 @@
+ar_readonly_exception = Active Record '{0}' is read only.
+ar_object_must_be_retrieved_before_delete = Active Record must be retrieved first before deletion.
+ar_object_must_not_be_null = Active record object must not be null.
+ar_object_marked_for_removal = Active record object already marked for removed.
+ar_column_meta_data_read_only = Column meta is read only.
+ar_invalid_database_driver = Active Record does not support database '{0}'.
+ar_invalid_finder_method = Unsupported Active Record finder method '{0}'.
+ar_no_primary_key_found = Table '{0}' does not contain any primary key fields.
+ar_primary_key_is_scalar = Primary key '{1}' in table '{0}' is NOT a composite key, invalid value '{2} used.
+ar_invalid_db_connection = Missing or invalid default database connection for ActiveRecord class '{0}', default connection is set by the DbConnection property of TActiveRecordManager.
+ar_mismatch_args_exception = ActiveRecord finder method '{0}' expects {1} parameters but found only {2} parameters instead.
+ar_invalid_tablename_property = Constant {0}::{1} must be a valid database table name.
+ar_invalid_tablename_method = Method {0}::{1} must return a valid database table name.
+ar_value_must_not_be_null = Property '{0}::${2}' must not be null as defined by column '{2}' in table '{1}'.
+ar_missing_pk_values = Missing primary key values in forming IN(key1, key2, ...) for table '{0}'.
+ar_pk_value_count_mismatch = Composite key value count mismatch in forming IN( (key1, key2, ..), (key3, key4, ..)) for table '{0}'.
+ar_must_copy_from_array_or_object = $data in {0}::copyFrom($data) must be an object or an array.
+ar_mismatch_column_names = In dynamic __call() method '{0}', no matching columns were found, valid columns for table '{2}' are '{1}'.
+ar_invalid_table = Missing, invalid or no permission for table/view '{0}'.
+ar_invalid_finder_class_name = Class name for finder($className) method must not be 'TActiveRecord', you should override the finder() method in your record class or pass in a valid record class name.
+ar_invalid_criteria = Invalid criteria object, must be a string or instance of TSqlCriteria.
+ar_relations_undefined = Unable to determine Active Record relationships because static array property {0}::${1} is not defined.
+ar_undefined_relation_prop = Unable to find {1}::${2}['{0}'], Active Record relationship definition for property "{0}" not found in entries of {1}::${2}.
+ar_invalid_relationship = Invalid active record relationship.
+ar_relations_missing_fk = Unable to find foreign key relationships in table '{0}' that corresponds to table '{1}'.
diff --git a/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php
new file mode 100644
index 0000000..4b0f89d
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php
@@ -0,0 +1,138 @@
+<?php
+/**
+ * TActiveRecordBelongsTo class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ */
+
+/**
+ * Loads base active record relationship class.
+ */
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
+
+/**
+ * Implements the foreign key relationship (TActiveRecord::BELONGS_TO) between
+ * the source objects and the related foreign object. Consider the
+ * <b>entity</b> relationship between a Team and a Player.
+ * <code>
+ * +------+ +--------+
+ * | Team | 1 <----- * | Player |
+ * +------+ +--------+
+ * </code>
+ * Where one team may have 0 or more players and each player belongs to only
+ * one team. We may model Team-Player <b>object</b> relationship as active record as follows.
+ * <code>
+ * class TeamRecord extends TActiveRecord
+ * {
+ * // see TActiveRecordHasMany for detailed definition.
+ * }
+ * class PlayerRecord extends TActiveRecord
+ * {
+ * const TABLE='player';
+ * public $player_id; //primary key
+ * public $team_name; //foreign key player.team_name <-> team.name
+ * public $age;
+ * public $team; //foreign object TeamRecord
+ *
+ * public static $RELATIONS = array
+ * (
+ * 'team' => array(self::BELONGS_TO, 'TeamRecord')
+ * );
+ *
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * </code>
+ * The static <tt>$RELATIONS</tt> property of PlayerRecord defines that the
+ * property <tt>$team</tt> belongs to a <tt>TeamRecord</tt>.
+ *
+ * The team object may be fetched as follows.
+ * <code>
+ * $players = PlayerRecord::finder()->with_team()->findAll();
+ * </code>
+ * The method <tt>with_xxx()</tt> (where <tt>xxx</tt> is the relationship property
+ * name, in this case, <tt>team</tt>) fetchs the corresponding TeamRecords using
+ * a second query (not by using a join). The <tt>with_xxx()</tt> accepts the same
+ * arguments as other finder methods of TActiveRecord, e.g.
+ * <tt>with_team('location = ?', 'Madrid')</tt>.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ * @since 3.1
+ */
+class TActiveRecordBelongsTo extends TActiveRecordRelation
+{
+ /**
+ * Get the foreign key index values from the results and make calls to the
+ * database to find the corresponding foreign objects.
+ * @param array original results.
+ */
+ protected function collectForeignObjects(&$results)
+ {
+ $fkeys = $this->getRelationForeignKeys();
+
+ $properties = array_keys($fkeys);
+ $fields = array_values($fkeys);
+ $indexValues = $this->getIndexValues($properties, $results);
+ $fkObjects = $this->findForeignObjects($fields, $indexValues);
+ $this->populateResult($results,$properties,$fkObjects,$fields);
+ }
+
+ /**
+ * @return array foreign key field names as key and object properties as value.
+ * @since 3.1.2
+ */
+ public function getRelationForeignKeys()
+ {
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ return $this->findForeignKeys($this->getSourceRecord(),$fkObject);
+ }
+
+ /**
+ * Sets the foreign objects to the given property on the source object.
+ * @param TActiveRecord source object.
+ * @param array foreign objects.
+ */
+ protected function setObjectProperty($source, $properties, &$collections)
+ {
+ $hash = $this->getObjectHash($source, $properties);
+ $prop = $this->getContext()->getProperty();
+ if(isset($collections[$hash]) && count($collections[$hash]) > 0)
+ {
+ if(count($collections[$hash]) > 1)
+ throw new TActiveRecordException('ar_belongs_to_multiple_result');
+ $source->$prop=$collections[$hash][0];
+ }
+ else
+ $source->$prop=null;
+ }
+
+ /**
+ * Updates the source object first.
+ * @return boolean true if all update are success (including if no update was required), false otherwise .
+ */
+ public function updateAssociatedRecords()
+ {
+ $obj = $this->getContext()->getSourceRecord();
+ $fkObject = $obj->getColumnValue($this->getContext()->getProperty());
+ if($fkObject!==null)
+ {
+ $fkObject->save();
+ $source = $this->getSourceRecord();
+ $fkeys = $this->findForeignKeys($source, $fkObject);
+ foreach($fkeys as $srcKey => $fKey)
+ $source->setColumnValue($srcKey, $fkObject->getColumnValue($fKey));
+ return true;
+ }
+ return false;
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php
new file mode 100644
index 0000000..cf3a8fc
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * TActiveRecordHasMany class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ */
+
+/**
+ * Loads base active record relations class.
+ */
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
+
+/**
+ * Implements TActiveRecord::HAS_MANY relationship between the source object having zero or
+ * more foreign objects. Consider the <b>entity</b> relationship between a Team and a Player.
+ * <code>
+ * +------+ +--------+
+ * | Team | 1 <----- * | Player |
+ * +------+ +--------+
+ * </code>
+ * Where one team may have 0 or more players and each player belongs to only
+ * one team. We may model Team-Player <b>object</b> relationship as active record as follows.
+ * <code>
+ * class TeamRecord extends TActiveRecord
+ * {
+ * const TABLE='team';
+ * public $name; //primary key
+ * public $location;
+ *
+ * public $players=array(); //list of players
+ *
+ * public static $RELATIONS=array
+ * (
+ * 'players' => array(self::HAS_MANY, 'PlayerRecord')
+ * );
+ *
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * class PlayerRecord extends TActiveRecord
+ * {
+ * // see TActiveRecordBelongsTo for detailed definition
+ * }
+ * </code>
+ * The static <tt>$RELATIONS</tt> property of TeamRecord defines that the
+ * property <tt>$players</tt> has many <tt>PlayerRecord</tt>s.
+ *
+ * The players list may be fetched as follows.
+ * <code>
+ * $team = TeamRecord::finder()->with_players()->findAll();
+ * </code>
+ * The method <tt>with_xxx()</tt> (where <tt>xxx</tt> is the relationship property
+ * name, in this case, <tt>players</tt>) fetchs the corresponding PlayerRecords using
+ * a second query (not by using a join). The <tt>with_xxx()</tt> accepts the same
+ * arguments as other finder methods of TActiveRecord, e.g. <tt>with_players('age < ?', 35)</tt>.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ * @since 3.1
+ */
+class TActiveRecordHasMany extends TActiveRecordRelation
+{
+ /**
+ * Get the foreign key index values from the results and make calls to the
+ * database to find the corresponding foreign objects.
+ * @param array original results.
+ */
+ protected function collectForeignObjects(&$results)
+ {
+ $fkeys = $this->getRelationForeignKeys();
+
+ $properties = array_values($fkeys);
+ $fields = array_keys($fkeys);
+
+ $indexValues = $this->getIndexValues($properties, $results);
+ $fkObjects = $this->findForeignObjects($fields,$indexValues);
+ $this->populateResult($results,$properties,$fkObjects,$fields);
+ }
+
+ /**
+ * @return array foreign key field names as key and object properties as value.
+ * @since 3.1.2
+ */
+ public function getRelationForeignKeys()
+ {
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ return $this->findForeignKeys($fkObject, $this->getSourceRecord());
+ }
+
+ /**
+ * Updates the associated foreign objects.
+ * @return boolean true if all update are success (including if no update was required), false otherwise .
+ */
+ public function updateAssociatedRecords()
+ {
+ $obj = $this->getContext()->getSourceRecord();
+ $fkObjects = &$obj->{$this->getContext()->getProperty()};
+ $success=true;
+ if(($total = count($fkObjects))> 0)
+ {
+ $source = $this->getSourceRecord();
+ $fkeys = $this->findForeignKeys($fkObjects[0], $source);
+ for($i=0;$i<$total;$i++)
+ {
+ foreach($fkeys as $fKey => $srcKey)
+ $fkObjects[$i]->setColumnValue($fKey, $source->getColumnValue($srcKey));
+ $success = $fkObjects[$i]->save() && $success;
+ }
+ }
+ return $success;
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
new file mode 100644
index 0000000..4c638d0
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
@@ -0,0 +1,376 @@
+<?php
+/**
+ * TActiveRecordHasManyAssociation class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ */
+
+/**
+ * Loads base active record relations class.
+ */
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
+
+/**
+ * Implements the M-N (many to many) relationship via association table.
+ * Consider the <b>entity</b> relationship between Articles and Categories
+ * via the association table <tt>Article_Category</tt>.
+ * <code>
+ * +---------+ +------------------+ +----------+
+ * | Article | * -----> * | Article_Category | * <----- * | Category |
+ * +---------+ +------------------+ +----------+
+ * </code>
+ * Where one article may have 0 or more categories and each category may have 0
+ * or more articles. We may model Article-Category <b>object</b> relationship
+ * as active record as follows.
+ * <code>
+ * class ArticleRecord
+ * {
+ * const TABLE='Article';
+ * public $article_id;
+ *
+ * public $Categories=array(); //foreign object collection.
+ *
+ * public static $RELATIONS = array
+ * (
+ * 'Categories' => array(self::MANY_TO_MANY, 'CategoryRecord', 'Article_Category')
+ * );
+ *
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * class CategoryRecord
+ * {
+ * const TABLE='Category';
+ * public $category_id;
+ *
+ * public $Articles=array();
+ *
+ * public static $RELATIONS = array
+ * (
+ * 'Articles' => array(self::MANY_TO_MANY, 'ArticleRecord', 'Article_Category')
+ * );
+ *
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * </code>
+ *
+ * The static <tt>$RELATIONS</tt> property of ArticleRecord defines that the
+ * property <tt>$Categories</tt> has many <tt>CategoryRecord</tt>s. Similar, the
+ * static <tt>$RELATIONS</tt> property of CategoryRecord defines many ArticleRecords.
+ *
+ * The articles with categories list may be fetched as follows.
+ * <code>
+ * $articles = TeamRecord::finder()->withCategories()->findAll();
+ * </code>
+ * The method <tt>with_xxx()</tt> (where <tt>xxx</tt> is the relationship property
+ * name, in this case, <tt>Categories</tt>) fetchs the corresponding CategoryRecords using
+ * a second query (not by using a join). The <tt>with_xxx()</tt> accepts the same
+ * arguments as other finder methods of TActiveRecord.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ * @since 3.1
+ */
+class TActiveRecordHasManyAssociation extends TActiveRecordRelation
+{
+ private $_association;
+ private $_sourceTable;
+ private $_foreignTable;
+ private $_association_columns=array();
+
+ /**
+ * Get the foreign key index values from the results and make calls to the
+ * database to find the corresponding foreign objects using association table.
+ * @param array original results.
+ */
+ protected function collectForeignObjects(&$results)
+ {
+ list($sourceKeys, $foreignKeys) = $this->getRelationForeignKeys();
+ $properties = array_values($sourceKeys);
+ $indexValues = $this->getIndexValues($properties, $results);
+ $this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys);
+ }
+
+ /**
+ * @return array 2 arrays of source keys and foreign keys from the association table.
+ */
+ public function getRelationForeignKeys()
+ {
+ $association = $this->getAssociationTable();
+ $sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord(), true);
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ $foreignKeys = $this->findForeignKeys($association, $fkObject);
+ return array($sourceKeys, $foreignKeys);
+ }
+
+ /**
+ * @return TDbTableInfo association table information.
+ */
+ protected function getAssociationTable()
+ {
+ if($this->_association===null)
+ {
+ $gateway = $this->getSourceRecord()->getRecordGateway();
+ $conn = $this->getSourceRecord()->getDbConnection();
+ //table name may include the fk column name separated with a dot.
+ $table = explode('.', $this->getContext()->getAssociationTable());
+ if(count($table)>1)
+ {
+ $columns = preg_replace('/^\((.*)\)/', '\1', $table[1]);
+ $this->_association_columns = preg_split('/\s*[, ]\*/',$columns);
+ }
+ $this->_association = $gateway->getTableInfo($conn, $table[0]);
+ }
+ return $this->_association;
+ }
+
+ /**
+ * @return TDbTableInfo source table information.
+ */
+ protected function getSourceTable()
+ {
+ if($this->_sourceTable===null)
+ {
+ $gateway = $this->getSourceRecord()->getRecordGateway();
+ $this->_sourceTable = $gateway->getRecordTableInfo($this->getSourceRecord());
+ }
+ return $this->_sourceTable;
+ }
+
+ /**
+ * @return TDbTableInfo foreign table information.
+ */
+ protected function getForeignTable()
+ {
+ if($this->_foreignTable===null)
+ {
+ $gateway = $this->getSourceRecord()->getRecordGateway();
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ $this->_foreignTable = $gateway->getRecordTableInfo($fkObject);
+ }
+ return $this->_foreignTable;
+ }
+
+ /**
+ * @return TDataGatewayCommand
+ */
+ protected function getCommandBuilder()
+ {
+ return $this->getSourceRecord()->getRecordGateway()->getCommand($this->getSourceRecord());
+ }
+
+ /**
+ * @return TDataGatewayCommand
+ */
+ protected function getForeignCommandBuilder()
+ {
+ $obj = $this->getContext()->getForeignRecordFinder();
+ return $this->getSourceRecord()->getRecordGateway()->getCommand($obj);
+ }
+
+
+ /**
+ * Fetches the foreign objects using TActiveRecord::findAllByIndex()
+ * @param array field names
+ * @param array foreign key index values.
+ */
+ protected function fetchForeignObjects(&$results,$foreignKeys,$indexValues,$sourceKeys)
+ {
+ $criteria = $this->getCriteria();
+ $finder = $this->getContext()->getForeignRecordFinder();
+ $type = get_class($finder);
+ $command = $this->createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys);
+ $srcProps = array_keys($sourceKeys);
+ $collections=array();
+ foreach($this->getCommandBuilder()->onExecuteCommand($command, $command->query()) as $row)
+ {
+ $hash = $this->getObjectHash($row, $srcProps);
+ foreach($srcProps as $column)
+ unset($row[$column]);
+ $obj = $this->createFkObject($type,$row,$foreignKeys);
+ $collections[$hash][] = $obj;
+ }
+ $this->setResultCollection($results, $collections, array_values($sourceKeys));
+ }
+
+ /**
+ * @param string active record class name.
+ * @param array row data
+ * @param array foreign key column names
+ * @return TActiveRecord
+ */
+ protected function createFkObject($type,$row,$foreignKeys)
+ {
+ $obj = TActiveRecord::createRecord($type, $row);
+ if(count($this->_association_columns) > 0)
+ {
+ $i=0;
+ foreach($foreignKeys as $ref=>$fk)
+ $obj->setColumnValue($ref, $row[$this->_association_columns[$i++]]);
+ }
+ return $obj;
+ }
+
+ /**
+ * @param TSqlCriteria
+ * @param TTableInfo association table info
+ * @param array field names
+ * @param array field values
+ */
+ public function createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys)
+ {
+ $innerJoin = $this->getAssociationJoin($foreignKeys,$indexValues,$sourceKeys);
+ $fkTable = $this->getForeignTable()->getTableFullName();
+ $srcColumns = $this->getSourceColumns($sourceKeys);
+ if(($where=$criteria->getCondition())===null)
+ $where='1=1';
+ $sql = "SELECT {$fkTable}.*, {$srcColumns} FROM {$fkTable} {$innerJoin} WHERE {$where}";
+
+ $parameters = $criteria->getParameters()->toArray();
+ $ordering = $criteria->getOrdersBy();
+ $limit = $criteria->getLimit();
+ $offset = $criteria->getOffset();
+
+ $builder = $this->getForeignCommandBuilder()->getBuilder();
+ $command = $builder->applyCriterias($sql,$parameters,$ordering,$limit,$offset);
+ $this->getCommandBuilder()->onCreateCommand($command, $criteria);
+ return $command;
+ }
+
+ /**
+ * @param array source table column names.
+ * @return string comma separated source column names.
+ */
+ protected function getSourceColumns($sourceKeys)
+ {
+ $columns=array();
+ $table = $this->getAssociationTable();
+ $tableName = $table->getTableFullName();
+ $columnNames = array_merge(array_keys($sourceKeys),$this->_association_columns);
+ foreach($columnNames as $name)
+ $columns[] = $tableName.'.'.$table->getColumn($name)->getColumnName();
+ return implode(', ', $columns);
+ }
+
+ /**
+ * SQL inner join for M-N relationship via association table.
+ * @param array foreign table column key names.
+ * @param array source table index values.
+ * @param array source table column names.
+ * @return string inner join condition for M-N relationship via association table.
+ */
+ protected function getAssociationJoin($foreignKeys,$indexValues,$sourceKeys)
+ {
+ $refInfo= $this->getAssociationTable();
+ $fkInfo = $this->getForeignTable();
+
+ $refTable = $refInfo->getTableFullName();
+ $fkTable = $fkInfo->getTableFullName();
+
+ $joins = array();
+ $hasAssociationColumns = count($this->_association_columns) > 0;
+ $i=0;
+ foreach($foreignKeys as $ref=>$fk)
+ {
+ if($hasAssociationColumns)
+ $refField = $refInfo->getColumn($this->_association_columns[$i++])->getColumnName();
+ else
+ $refField = $refInfo->getColumn($ref)->getColumnName();
+ $fkField = $fkInfo->getColumn($fk)->getColumnName();
+ $joins[] = "{$fkTable}.{$fkField} = {$refTable}.{$refField}";
+ }
+ $joinCondition = implode(' AND ', $joins);
+ $index = $this->getCommandBuilder()->getIndexKeyCondition($refInfo,array_keys($sourceKeys), $indexValues);
+ return "INNER JOIN {$refTable} ON ({$joinCondition}) AND {$index}";
+ }
+
+ /**
+ * Updates the associated foreign objects.
+ * @return boolean true if all update are success (including if no update was required), false otherwise .
+ */
+ public function updateAssociatedRecords()
+ {
+ $obj = $this->getContext()->getSourceRecord();
+ $fkObjects = &$obj->{$this->getContext()->getProperty()};
+ $success=true;
+ if(($total = count($fkObjects))> 0)
+ {
+ $source = $this->getSourceRecord();
+ $builder = $this->getAssociationTableCommandBuilder();
+ for($i=0;$i<$total;$i++)
+ $success = $fkObjects[$i]->save() && $success;
+ return $this->updateAssociationTable($obj, $fkObjects, $builder) && $success;
+ }
+ return $success;
+ }
+
+ /**
+ * @return TDbCommandBuilder
+ */
+ protected function getAssociationTableCommandBuilder()
+ {
+ $conn = $this->getContext()->getSourceRecord()->getDbConnection();
+ return $this->getAssociationTable()->createCommandBuilder($conn);
+ }
+
+ private function hasAssociationData($builder,$data)
+ {
+ $condition=array();
+ $table = $this->getAssociationTable();
+ foreach($data as $name=>$value)
+ $condition[] = $table->getColumn($name)->getColumnName().' = ?';
+ $command = $builder->createCountCommand(implode(' AND ', $condition),array_values($data));
+ $result = $this->getCommandBuilder()->onExecuteCommand($command, intval($command->queryScalar()));
+ return intval($result) > 0;
+ }
+
+ private function addAssociationData($builder,$data)
+ {
+ $command = $builder->createInsertCommand($data);
+ return $this->getCommandBuilder()->onExecuteCommand($command, $command->execute()) > 0;
+ }
+
+ private function updateAssociationTable($obj,$fkObjects, $builder)
+ {
+ $source = $this->getSourceRecordValues($obj);
+ $foreignKeys = $this->findForeignKeys($this->getAssociationTable(), $fkObjects[0]);
+ $success=true;
+ foreach($fkObjects as $fkObject)
+ {
+ $data = array_merge($source, $this->getForeignObjectValues($foreignKeys,$fkObject));
+ if(!$this->hasAssociationData($builder,$data))
+ $success = $this->addAssociationData($builder,$data) && $success;
+ }
+ return $success;
+ }
+
+ private function getSourceRecordValues($obj)
+ {
+ $sourceKeys = $this->findForeignKeys($this->getAssociationTable(), $obj);
+ $indexValues = $this->getIndexValues(array_values($sourceKeys), $obj);
+ $data = array();
+ $i=0;
+ foreach($sourceKeys as $name=>$srcKey)
+ $data[$name] = $indexValues[0][$i++];
+ return $data;
+ }
+
+ private function getForeignObjectValues($foreignKeys,$fkObject)
+ {
+ $data=array();
+ foreach($foreignKeys as $name=>$fKey)
+ $data[$name] = $fkObject->getColumnValue($fKey);
+ return $data;
+ }
+}
diff --git a/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php
new file mode 100644
index 0000000..2b9ada2
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * TActiveRecordHasOne class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ */
+
+/**
+ * Loads base active record relationship class.
+ */
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
+
+/**
+ * TActiveRecordHasOne models the object relationship that a record (the source object)
+ * property is an instance of foreign record object having a foreign key
+ * related to the source object. The HAS_ONE relation is very similar to the
+ * HAS_MANY relationship (in fact, it is equivalent in the entities relationship point of view).
+ *
+ * The difference of HAS_ONE from HAS_MANY is that the foreign object is singular.
+ * That is, HAS_MANY will return a collection of records while HAS_ONE returns the
+ * corresponding record.
+ *
+ * Consider the <b>entity</b> relationship between a Car and a Engine.
+ * <code>
+ * +-----+ +--------+
+ * | Car | 1 <----- 1 | Engine |
+ * +-----+ +--------+
+ * </code>
+ * Where each engine belongs to only one car, that is, the Engine entity has
+ * a foreign key to the Car's primary key. We may model
+ * Engine-Car <b>object</b> relationship as active record as follows.
+ * <code>
+ * class CarRecord extends TActiveRecord
+ * {
+ * const TABLE='car';
+ * public $car_id; //primary key
+ * public $colour;
+ *
+ * public $engine; //engine foreign object
+ *
+ * public static $RELATIONS=array
+ * (
+ * 'engine' => array(self::HAS_ONE, 'EngineRecord')
+ * );
+ *
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * class EngineRecord extends TActiveRecord
+ * {
+ * const TABLE='engine';
+ * public $engine_id;
+ * public $capacity;
+ * public $car_id; //foreign key to cars
+ *
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * </code>
+ * The static <tt>$RELATIONS</tt> property of CarRecord defines that the
+ * property <tt>$engine</tt> that will reference an <tt>EngineRecord</tt> instance.
+ *
+ * The car record with engine property list may be fetched as follows.
+ * <code>
+ * $cars = CarRecord::finder()->with_engine()->findAll();
+ * </code>
+ * The method <tt>with_xxx()</tt> (where <tt>xxx</tt> is the relationship property
+ * name, in this case, <tt>engine</tt>) fetchs the corresponding EngineRecords using
+ * a second query (not by using a join). The <tt>with_xxx()</tt> accepts the same
+ * arguments as other finder methods of TActiveRecord, e.g. <tt>with_engine('capacity < ?', 3.8)</tt>.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ * @since 3.1
+ */
+class TActiveRecordHasOne extends TActiveRecordRelation
+{
+ /**
+ * Get the foreign key index values from the results and make calls to the
+ * database to find the corresponding foreign objects.
+ * @param array original results.
+ */
+ protected function collectForeignObjects(&$results)
+ {
+ $fkeys = $this->getRelationForeignKeys();
+ $properties = array_values($fkeys);
+ $fields = array_keys($fkeys);
+
+ $indexValues = $this->getIndexValues($properties, $results);
+ $fkObjects = $this->findForeignObjects($fields,$indexValues);
+ $this->populateResult($results,$properties,$fkObjects,$fields);
+ }
+
+ /**
+ * @return array foreign key field names as key and object properties as value.
+ * @since 3.1.2
+ */
+ public function getRelationForeignKeys()
+ {
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ return $this->findForeignKeys($fkObject, $this->getSourceRecord());
+ }
+
+ /**
+ * Sets the foreign objects to the given property on the source object.
+ * @param TActiveRecord source object.
+ * @param array foreign objects.
+ */
+ protected function setObjectProperty($source, $properties, &$collections)
+ {
+ $hash = $this->getObjectHash($source, $properties);
+ $prop = $this->getContext()->getProperty();
+ if(isset($collections[$hash]) && count($collections[$hash]) > 0)
+ {
+ if(count($collections[$hash]) > 1)
+ throw new TActiveRecordException('ar_belongs_to_multiple_result');
+ $source->setColumnValue($prop, $collections[$hash][0]);
+ }
+ }
+
+ /**
+ * Updates the associated foreign objects.
+ * @return boolean true if all update are success (including if no update was required), false otherwise .
+ */
+ public function updateAssociatedRecords()
+ {
+ $fkObject = $this->getContext()->getPropertyValue();
+ $source = $this->getSourceRecord();
+ $fkeys = $this->findForeignKeys($fkObject, $source);
+ foreach($fkeys as $fKey => $srcKey)
+ $fkObject->setColumnValue($fKey, $source->getColumnValue($srcKey));
+ return $fkObject->save();
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php
new file mode 100644
index 0000000..1b577f0
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php
@@ -0,0 +1,254 @@
+<?php
+/**
+ * TActiveRecordRelation class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ */
+
+/**
+ * Load active record relationship context.
+ */
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext');
+
+/**
+ * Base class for active record relationships.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ * @since 3.1
+ */
+abstract class TActiveRecordRelation
+{
+ private $_context;
+ private $_criteria;
+
+ public function __construct(TActiveRecordRelationContext $context, $criteria)
+ {
+ $this->_context = $context;
+ $this->_criteria = $criteria;
+ }
+
+ /**
+ * @return TActiveRecordRelationContext
+ */
+ protected function getContext()
+ {
+ return $this->_context;
+ }
+
+ /**
+ * @return TActiveRecordCriteria
+ */
+ protected function getCriteria()
+ {
+ return $this->_criteria;
+ }
+
+ /**
+ * @return TActiveRecord
+ */
+ protected function getSourceRecord()
+ {
+ return $this->getContext()->getSourceRecord();
+ }
+
+ abstract protected function collectForeignObjects(&$results);
+
+ /**
+ * Dispatch the method calls to the source record finder object. When
+ * an instance of TActiveRecord or an array of TActiveRecord is returned
+ * the corresponding foreign objects are also fetched and assigned.
+ *
+ * Multiple relationship calls can be chain together.
+ *
+ * @param string method name called
+ * @param array method arguments
+ * @return mixed TActiveRecord or array of TActiveRecord results depending on the method called.
+ */
+ public function __call($method,$args)
+ {
+ static $stack=array();
+
+ $results = call_user_func_array(array($this->getSourceRecord(),$method),$args);
+ $validArray = is_array($results) && count($results) > 0;
+ if($validArray || $results instanceof ArrayAccess || $results instanceof TActiveRecord)
+ {
+ $this->collectForeignObjects($results);
+ while($obj = array_pop($stack))
+ $obj->collectForeignObjects($results);
+ }
+ else if($results instanceof TActiveRecordRelation)
+ $stack[] = $this; //call it later
+ else if($results === null || !$validArray)
+ $stack = array();
+ return $results;
+ }
+
+ /**
+ * Fetch results for current relationship.
+ * @return boolean always true.
+ */
+ public function fetchResultsInto($obj)
+ {
+ $this->collectForeignObjects($obj);
+ return true;
+ }
+
+ /**
+ * Returns foreign keys in $fromRecord with source column names as key
+ * and foreign column names in the corresponding $matchesRecord as value.
+ * The method returns the first matching foreign key between these 2 records.
+ * @param TActiveRecord $fromRecord
+ * @param TActiveRecord $matchesRecord
+ * @return array foreign keys with source column names as key and foreign column names as value.
+ */
+ protected function findForeignKeys($from, $matchesRecord, $loose=false)
+ {
+ $gateway = $matchesRecord->getRecordGateway();
+ $recordTableInfo = $gateway->getRecordTableInfo($matchesRecord);
+ $matchingTableName = strtolower($recordTableInfo->getTableName());
+ $matchingFullTableName = strtolower($recordTableInfo->getTableFullName());
+ $tableInfo=$from;
+ if($from instanceof TActiveRecord)
+ $tableInfo = $gateway->getRecordTableInfo($from);
+ //find first non-empty FK
+ foreach($tableInfo->getForeignKeys() as $fkeys)
+ {
+ $fkTable = strtolower($fkeys['table']);
+ if($fkTable===$matchingTableName || $fkTable===$matchingFullTableName)
+ {
+ $hasFkField = !$loose && $this->getContext()->hasFkField();
+ $key = $hasFkField ? $this->getFkFields($fkeys['keys']) : $fkeys['keys'];
+ if(!empty($key))
+ return $key;
+ }
+ }
+
+ //none found
+ $matching = $gateway->getRecordTableInfo($matchesRecord)->getTableFullName();
+ throw new TActiveRecordException('ar_relations_missing_fk',
+ $tableInfo->getTableFullName(), $matching);
+ }
+
+ /**
+ * @return array foreign key field names as key and object properties as value.
+ * @since 3.1.2
+ */
+ abstract public function getRelationForeignKeys();
+
+ /**
+ * Find matching foreign key fields from the 3rd element of an entry in TActiveRecord::$RELATION.
+ * Assume field names consist of [\w-] character sets. Prefix to the field names ending with a dot
+ * are ignored.
+ */
+ private function getFkFields($fkeys)
+ {
+ $matching = array();
+ preg_match_all('/\s*(\S+\.)?([\w-]+)\s*/', $this->getContext()->getFkField(), $matching);
+ $fields = array();
+ foreach($fkeys as $fkName => $field)
+ {
+ if(in_array($fkName, $matching[2]))
+ $fields[$fkName] = $field;
+ }
+ return $fields;
+ }
+
+ /**
+ * @param mixed object or array to be hashed
+ * @param array name of property for hashing the properties.
+ * @return string object hash using crc32 and serialize.
+ */
+ protected function getObjectHash($obj, $properties)
+ {
+ $ids=array();
+ foreach($properties as $property)
+ $ids[] = is_object($obj) ? (string)$obj->getColumnValue($property) : (string)$obj[$property];
+ return serialize($ids);
+ }
+
+ /**
+ * Fetches the foreign objects using TActiveRecord::findAllByIndex()
+ * @param array field names
+ * @param array foreign key index values.
+ * @return TActiveRecord[] foreign objects.
+ */
+ protected function findForeignObjects($fields, $indexValues)
+ {
+ $finder = $this->getContext()->getForeignRecordFinder();
+ return $finder->findAllByIndex($this->_criteria, $fields, $indexValues);
+ }
+
+ /**
+ * Obtain the foreign key index values from the results.
+ * @param array property names
+ * @param array TActiveRecord results
+ * @return array foreign key index values.
+ */
+ protected function getIndexValues($keys, $results)
+ {
+ if(!is_array($results) && !$results instanceof ArrayAccess)
+ $results = array($results);
+ $values=array();
+ foreach($results as $result)
+ {
+ $value = array();
+ foreach($keys as $name)
+ $value[] = $result->getColumnValue($name);
+ $values[] = $value;
+ }
+ return $values;
+ }
+
+ /**
+ * Populate the results with the foreign objects found.
+ * @param array source results
+ * @param array source property names
+ * @param array foreign objects
+ * @param array foreign object field names.
+ */
+ protected function populateResult(&$results,$properties,&$fkObjects,$fields)
+ {
+ $collections=array();
+ foreach($fkObjects as $fkObject)
+ $collections[$this->getObjectHash($fkObject, $fields)][]=$fkObject;
+ $this->setResultCollection($results, $collections, $properties);
+ }
+
+ /**
+ * Populates the result array with foreign objects (matched using foreign key hashed property values).
+ * @param array $results
+ * @param array $collections
+ * @param array property names
+ */
+ protected function setResultCollection(&$results, &$collections, $properties)
+ {
+ if(is_array($results) || $results instanceof ArrayAccess)
+ {
+ for($i=0,$k=count($results);$i<$k;$i++)
+ $this->setObjectProperty($results[$i], $properties, $collections);
+ }
+ else
+ $this->setObjectProperty($results, $properties, $collections);
+ }
+
+ /**
+ * Sets the foreign objects to the given property on the source object.
+ * @param TActiveRecord source object.
+ * @param array source properties
+ * @param array foreign objects.
+ */
+ protected function setObjectProperty($source, $properties, &$collections)
+ {
+ $hash = $this->getObjectHash($source, $properties);
+ $prop = $this->getContext()->getProperty();
+ $source->$prop=isset($collections[$hash]) ? $collections[$hash] : array();
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php
new file mode 100644
index 0000000..24fdee5
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php
@@ -0,0 +1,230 @@
+<?php
+/**
+ * TActiveRecordRelationContext class.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ */
+
+/**
+ * TActiveRecordRelationContext holds information regarding record relationships
+ * such as record relation property name, query criteria and foreign object record
+ * class names.
+ *
+ * This class is use internally by passing a context to the TActiveRecordRelation
+ * constructor.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Relations
+ * @since 3.1
+ */
+class TActiveRecordRelationContext
+{
+ private $_property;
+ private $_record;
+ private $_relation; //data from an entry of TActiveRecord::$RELATION
+ private $_fkeys;
+
+ public function __construct($record, $property=null, $relation=null)
+ {
+ $this->_record=$record;
+ $this->_property=$property;
+ $this->_relation=$relation;
+ }
+
+ /**
+ * @return boolean true if the relation is defined in TActiveRecord::$RELATIONS
+ * @since 3.1.2
+ */
+ public function hasRecordRelation()
+ {
+ return $this->_relation!==null;
+ }
+
+ public function getPropertyValue()
+ {
+ $obj = $this->getSourceRecord();
+ return $obj->getColumnValue($this->getProperty());
+ }
+
+ /**
+ * @return string name of the record property that the relationship results will be assigned to.
+ */
+ public function getProperty()
+ {
+ return $this->_property;
+ }
+
+ /**
+ * @return TActiveRecord the active record instance that queried for its related records.
+ */
+ public function getSourceRecord()
+ {
+ return $this->_record;
+ }
+
+ /**
+ * @return array foreign key of this relations, the keys is dependent on the
+ * relationship type.
+ * @since 3.1.2
+ */
+ public function getRelationForeignKeys()
+ {
+ if($this->_fkeys===null)
+ $this->_fkeys=$this->getRelationHandler()->getRelationForeignKeys();
+ return $this->_fkeys;
+ }
+
+ /**
+ * @return string HAS_MANY, HAS_ONE, or BELONGS_TO
+ */
+ public function getRelationType()
+ {
+ return $this->_relation[0];
+ }
+
+ /**
+ * @return string foreign record class name.
+ */
+ public function getForeignRecordClass()
+ {
+ return $this->_relation[1];
+ }
+
+ /**
+ * @return string foreign key field names, comma delimited.
+ * @since 3.1.2
+ */
+ public function getFkField()
+ {
+ return $this->_relation[2];
+ }
+
+ /**
+ * @return string the query condition for the relation as specified in RELATIONS
+ * @since 3.1.2
+ */
+ public function getCondition()
+ {
+ return isset($this->_relation[3])?$this->_relation[3]:null;
+ }
+
+ /**
+ * @return array the query parameters for the relation as specified in RELATIONS
+ * @since 3.1.2
+ */
+ public function getParameters()
+ {
+ return isset($this->_relation[4])?$this->_relation[4]:array();
+ }
+
+ /**
+ * @return boolean true if the 3rd element of an TActiveRecord::$RELATION entry is set.
+ * @since 3.1.2
+ */
+ public function hasFkField()
+ {
+ $notManyToMany = $this->getRelationType() !== TActiveRecord::MANY_TO_MANY;
+ return $notManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]);
+ }
+
+ /**
+ * @return string the M-N relationship association table name.
+ */
+ public function getAssociationTable()
+ {
+ return $this->_relation[2];
+ }
+
+ /**
+ * @return boolean true if the relationship is HAS_MANY and requires an association table.
+ */
+ public function hasAssociationTable()
+ {
+ $isManyToMany = $this->getRelationType() === TActiveRecord::MANY_TO_MANY;
+ return $isManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]);
+ }
+
+ /**
+ * @return TActiveRecord corresponding relationship foreign object finder instance.
+ */
+ public function getForeignRecordFinder()
+ {
+ return TActiveRecord::finder($this->getForeignRecordClass());
+ }
+
+ /**
+ * Creates and return the TActiveRecordRelation handler for specific relationships.
+ * An instance of TActiveRecordHasOne, TActiveRecordBelongsTo, TActiveRecordHasMany,
+ * or TActiveRecordHasManyAssocation will be returned.
+ * @param TActiveRecordCriteria search criteria
+ * @return TActiveRecordRelation record relationship handler instnace.
+ * @throws TActiveRecordException if property is not defined or missing.
+ */
+ public function getRelationHandler($criteria=null)
+ {
+ if(!$this->hasRecordRelation())
+ {
+ throw new TActiveRecordException('ar_undefined_relation_prop',
+ $this->_property, get_class($this->_record), 'RELATIONS');
+ }
+ if($criteria===null)
+ $criteria = new TActiveRecordCriteria($this->getCondition(), $this->getParameters());
+ switch($this->getRelationType())
+ {
+ case TActiveRecord::HAS_MANY:
+ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasMany');
+ return new TActiveRecordHasMany($this, $criteria);
+ case TActiveRecord::MANY_TO_MANY:
+ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasManyAssociation');
+ return new TActiveRecordHasManyAssociation($this, $criteria);
+ case TActiveRecord::HAS_ONE:
+ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasOne');
+ return new TActiveRecordHasOne($this, $criteria);
+ case TActiveRecord::BELONGS_TO:
+ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordBelongsTo');
+ return new TActiveRecordBelongsTo($this, $criteria);
+ default:
+ throw new TActiveRecordException('ar_invalid_relationship');
+ }
+ }
+
+ /**
+ * @return TActiveRecordRelationCommand
+ */
+ public function updateAssociatedRecords($updateBelongsTo=false)
+ {
+ $success=true;
+ foreach($this->_record->getRecordRelations() as $data)
+ {
+ list($property, $relation) = $data;
+ $belongsTo = $relation[0]==TActiveRecord::BELONGS_TO;
+ if(($updateBelongsTo && $belongsTo) || (!$updateBelongsTo && !$belongsTo))
+ {
+ $obj = $this->getSourceRecord();
+ if(!$this->isEmptyFkObject($obj->getColumnValue($property)))
+ {
+ $context = new TActiveRecordRelationContext($this->getSourceRecord(),$property,$relation);
+ $success = $context->getRelationHandler()->updateAssociatedRecords() && $success;
+ }
+ }
+ }
+ return $success;
+ }
+
+ protected function isEmptyFkObject($obj)
+ {
+ if(is_object($obj))
+ return $obj instanceof TList ? $obj->count() === 0 : false;
+ else if(is_array($obj))
+ return count($obj)===0;
+ else
+ return empty($obj);
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TIbmScaffoldInput.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TIbmScaffoldInput.php
new file mode 100644
index 0000000..47b6797
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TIbmScaffoldInput.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * TIbmScaffoldInput class file.
+ *
+ * @author Cesar Ramos <cramos[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold.InputBuilder
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.InputBuilder.TScaffoldInputCommon');
+
+class TIbmScaffoldInput extends TScaffoldInputCommon
+{
+ protected function createControl($container, $column, $record)
+ {
+ switch(strtolower($column->getDbType()))
+ {
+ case 'date':
+ return $this->createDateControl($container, $column, $record);
+ case 'time':
+ return $this->createTimeControl($container, $column, $record);
+ case 'timestamp':
+ return $this->createDateTimeControl($container, $column, $record);
+ case 'smallint': case 'integer': case 'bigint':
+ return $this->createIntegerControl($container, $column, $record);
+ case 'decimal': case 'numeric': case 'real': case 'float': case 'double':
+ return $this->createFloatControl($container, $column, $record);
+ case 'char': case 'varchar':
+ return $this->createMultiLineControl($container, $column, $record);
+ default:
+ return $this->createDefaultControl($container,$column, $record);
+ }
+ }
+
+ protected function getControlValue($container, $column, $record)
+ {
+ switch(strtolower($column->getDbType()))
+ {
+ case 'date':
+ return $container->findControl(self::DEFAULT_ID)->getDate();
+ case 'time':
+ return $this->getTimeValue($container, $column, $record);
+ case 'timestamp':
+ return $this->getDateTimeValue($container, $column, $record);
+ default:
+ return $this->getDefaultControlValue($container,$column, $record);
+ }
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TMssqlScaffoldInput.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TMssqlScaffoldInput.php
new file mode 100644
index 0000000..3ebc0c7
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TMssqlScaffoldInput.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * TMssqlScaffoldInput class file.
+ *
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold.InputBuilder
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.InputBuilder.TScaffoldInputCommon');
+
+class TMssqlScaffoldInput extends TScaffoldInputCommon
+{
+ protected function createControl($container, $column, $record)
+ {
+ switch(strtolower($column->getDbType()))
+ {
+ case 'bit':
+ return $this->createBooleanControl($container, $column, $record);
+ case 'text':
+ return $this->createMultiLineControl($container, $column, $record);
+ case 'smallint': case 'int': case 'bigint': case 'tinyint':
+ return $this->createIntegerControl($container, $column, $record);
+ case 'decimal': case 'float': case 'money': case 'numeric': case 'real': case 'smallmoney':
+ return $this->createFloatControl($container, $column, $record);
+ case 'datetime': case 'smalldatetime':
+ return $this->createDateTimeControl($container, $column, $record);
+ default:
+ $control = $this->createDefaultControl($container,$column, $record);
+ if($column->getIsExcluded())
+ $control->setEnabled(false);
+ return $control;
+ }
+ }
+
+ protected function getControlValue($container, $column, $record)
+ {
+ switch(strtolower($column->getDbType()))
+ {
+ case 'boolean':
+ return $container->findControl(self::DEFAULT_ID)->getChecked();
+ case 'datetime': case 'smalldatetime':
+ return $this->getDateTimeValue($container,$column, $record);
+ default:
+ $value = $this->getDefaultControlValue($container,$column, $record);
+ if(trim($value)==='' && $column->getAllowNull())
+ return null;
+ else
+ return $value;
+ }
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TMysqlScaffoldInput.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TMysqlScaffoldInput.php
new file mode 100644
index 0000000..6885461
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TMysqlScaffoldInput.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * TMysqlScaffoldInput class file.
+ *
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold.InputBuilder
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.InputBuilder.TScaffoldInputCommon');
+
+class TMysqlScaffoldInput extends TScaffoldInputCommon
+{
+ protected function createControl($container, $column, $record)
+ {
+ $dbtype = trim(str_replace(array('unsigned', 'zerofill'),array('','',),strtolower($column->getDbType())));
+ switch($dbtype)
+ {
+ case 'date':
+ return $this->createDateControl($container, $column, $record);
+ case 'blob': case 'tinyblob': case 'mediumblob': case 'longblob':
+ case 'text': case 'tinytext': case 'mediumtext': case 'longtext':
+ return $this->createMultiLineControl($container, $column, $record);
+ case 'year':
+ return $this->createYearControl($container, $column, $record);
+ case 'int': case 'integer': case 'tinyint': case 'smallint': case 'mediumint': case 'bigint':
+ return $this->createIntegerControl($container, $column, $record);
+ case 'decimal': case 'double': case 'float':
+ return $this->createFloatControl($container, $column, $record);
+ case 'time' :
+ return $this->createTimeControl($container, $column, $record);
+ case 'datetime': case 'timestamp':
+ return $this->createDateTimeControl($container, $column, $record);
+ case 'set':
+ return $this->createSetControl($container, $column, $record);
+ case 'enum':
+ return $this->createEnumControl($container, $column, $record);
+ default:
+ return $this->createDefaultControl($container, $column, $record);
+ }
+ }
+
+ protected function getControlValue($container, $column, $record)
+ {
+ $dbtype = trim(str_replace(array('unsigned', 'zerofill'),array('','',),strtolower($column->getDbType())));
+ switch($dbtype)
+ {
+ case 'date':
+ return $container->findControl(self::DEFAULT_ID)->getDate();
+ case 'year':
+ return $container->findControl(self::DEFAULT_ID)->getSelectedValue();
+ case 'time':
+ return $this->getTimeValue($container, $column, $record);
+ case 'datetime': case 'timestamp':
+ return $this->getDateTimeValue($container,$column, $record);
+ case 'tinyint':
+ return $this->getIntBooleanValue($container,$column, $record);
+ case 'set':
+ return $this->getSetValue($container, $column, $record);
+ case 'enum':
+ return $this->getEnumValue($container, $column, $record);
+ default:
+ return $this->getDefaultControlValue($container,$column, $record);
+ }
+ }
+
+ protected function createIntegerControl($container, $column, $record)
+ {
+ if($column->getColumnSize()==1)
+ return $this->createBooleanControl($container, $column, $record);
+ else
+ parent::createIntegerControl($container, $column, $record);
+ }
+
+ protected function getIntBooleanValue($container,$column, $record)
+ {
+ if($column->getColumnSize()==1)
+ return (int)$container->findControl(self::DEFAULT_ID)->getChecked();
+ else
+ return $this->getDefaultControlValue($container,$column, $record);
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TPgsqlScaffoldInput.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TPgsqlScaffoldInput.php
new file mode 100644
index 0000000..312f34d
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TPgsqlScaffoldInput.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * TPgsqlScaffoldInput class file.
+ *
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold.InputBuilder
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.InputBuilder.TScaffoldInputCommon');
+
+class TPgsqlScaffoldInput extends TScaffoldInputCommon
+{
+ protected function createControl($container, $column, $record)
+ {
+ switch(strtolower($column->getDbType()))
+ {
+ case 'boolean':
+ return $this->createBooleanControl($container, $column, $record);
+ case 'date':
+ return $this->createDateControl($container, $column, $record);
+ case 'text':
+ return $this->createMultiLineControl($container, $column, $record);
+ case 'smallint': case 'integer': case 'bigint':
+ return $this->createIntegerControl($container, $column, $record);
+ case 'decimal': case 'numeric': case 'real': case 'double precision':
+ return $this->createFloatControl($container, $column, $record);
+ case 'time without time zone' :
+ return $this->createTimeControl($container, $column, $record);
+ case 'timestamp without time zone':
+ return $this->createDateTimeControl($container, $column, $record);
+ default:
+ return $this->createDefaultControl($container,$column, $record);
+ }
+ }
+
+ protected function getControlValue($container, $column, $record)
+ {
+ switch(strtolower($column->getDbType()))
+ {
+ case 'boolean':
+ return $container->findControl(self::DEFAULT_ID)->getChecked();
+ case 'date':
+ return $container->findControl(self::DEFAULT_ID)->getDate();
+ case 'time without time zone':
+ return $this->getTimeValue($container, $column, $record);
+ case 'timestamp without time zone':
+ return $this->getDateTimeValue($container,$column, $record);
+ default:
+ return $this->getDefaultControlValue($container,$column, $record);
+ }
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TScaffoldInputBase.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TScaffoldInputBase.php
new file mode 100644
index 0000000..bb1715a
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TScaffoldInputBase.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * TScaffoldInputBase class file.
+ *
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold.InputBuilder
+ */
+class TScaffoldInputBase
+{
+ const DEFAULT_ID = 'scaffold_input';
+ private $_parent;
+
+ protected function getParent()
+ {
+ return $this->_parent;
+ }
+
+ public static function createInputBuilder($record)
+ {
+ $record->getDbConnection()->setActive(true); //must be connected before retrieving driver name!
+ $driver = $record->getDbConnection()->getDriverName();
+ switch(strtolower($driver))
+ {
+ case 'sqlite': //sqlite 3
+ case 'sqlite2': //sqlite 2
+ require_once(dirname(__FILE__).'/TSqliteScaffoldInput.php');
+ return new TSqliteScaffoldInput($conn);
+ case 'mysqli':
+ case 'mysql':
+ require_once(dirname(__FILE__).'/TMysqlScaffoldInput.php');
+ return new TMysqlScaffoldInput($conn);
+ case 'pgsql':
+ require_once(dirname(__FILE__).'/TPgsqlScaffoldInput.php');
+ return new TPgsqlScaffoldInput($conn);
+ case 'mssql':
+ require_once(dirname(__FILE__).'/TMssqlScaffoldInput.php');
+ return new TMssqlScaffoldInput($conn);
+ case 'ibm':
+ require_once(dirname(__FILE__).'/TIbmScaffoldInput.php');
+ return new TIbmScaffoldInput($conn);
+ default:
+ throw new TConfigurationException(
+ 'scaffold_invalid_database_driver',$driver);
+ }
+ }
+
+ public function createScaffoldInput($parent, $item, $column, $record)
+ {
+ $this->_parent=$parent;
+ $item->setCustomData($column->getColumnId());
+ $this->createControl($item->_input, $column, $record);
+ if($item->_input->findControl(self::DEFAULT_ID))
+ $this->createControlLabel($item->_label, $column, $record);
+ }
+
+ protected function createControlLabel($label, $column, $record)
+ {
+ $fieldname = ucwords(str_replace('_', ' ', $column->getColumnId())).':';
+ $label->setText($fieldname);
+ $label->setForControl(self::DEFAULT_ID);
+ }
+
+ public function loadScaffoldInput($parent, $item, $column, $record)
+ {
+ $this->_parent=$parent;
+ if($this->getIsEnabled($column, $record))
+ {
+ $prop = $column->getColumnId();
+ $record->setColumnValue($prop, $this->getControlValue($item->_input, $column, $record));
+ }
+ }
+
+ protected function getIsEnabled($column, $record)
+ {
+ return !($this->getParent()->getRecordPk() !== null
+ && $column->getIsPrimaryKey() || $column->hasSequence());
+ }
+
+ protected function getRecordPropertyValue($column, $record)
+ {
+ $value = $record->getColumnValue($column->getColumnId());
+ if($column->getDefaultValue()!==TDbTableColumn::UNDEFINED_VALUE && $value===null)
+ return $column->getDefaultValue();
+ else
+ return $value;
+ }
+
+ protected function setRecordPropertyValue($item, $record, $input)
+ {
+ $record->setColumnValue($item->getCustomData(), $input->getText());
+ }
+
+ protected function createControl($container, $column, $record)
+ {
+ }
+
+ protected function getControlValue($container, $column, $record)
+ {
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TScaffoldInputCommon.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TScaffoldInputCommon.php
new file mode 100644
index 0000000..1805aff
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TScaffoldInputCommon.php
@@ -0,0 +1,309 @@
+<?php
+/**
+ * TScaffoldInputCommon class file.
+ *
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold.InputBuilder
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.InputBuilder.TScaffoldInputBase');
+
+class TScaffoldInputCommon extends TScaffoldInputBase
+{
+ protected function setDefaultProperty($container, $control, $column, $record)
+ {
+ $control->setID(self::DEFAULT_ID);
+ $control->setEnabled($this->getIsEnabled($column, $record));
+ $container->Controls[] = $control;
+ }
+
+ protected function setNotNullProperty($container, $control, $column, $record)
+ {
+ $this->setDefaultProperty($container, $control, $column, $record);
+ if(!$column->getAllowNull() && !$column->hasSequence())
+ $this->createRequiredValidator($container, $column, $record);
+ }
+
+ protected function createBooleanControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $control = new TCheckBox();
+ $control->setChecked(TPropertyValue::ensureBoolean($value));
+ $control->setCssClass('boolean-checkbox');
+ $this->setDefaultProperty($container, $control, $column, $record);
+ return $control;
+ }
+
+ protected function createDefaultControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $control = new TTextBox();
+ $control->setText($value);
+ $control->setCssClass('default-textbox scaffold_input');
+ if(($len=$column->getColumnSize())!==null)
+ $control->setMaxLength($len);
+ $this->setNotNullProperty($container, $control, $column, $record);
+ return $control;
+ }
+
+ protected function getDefaultControlValue($container,$column, $record)
+ {
+ $control = $container->findControl(self::DEFAULT_ID);
+ if($control instanceof TCheckBox)
+ return $control->getChecked();
+ else if($control instanceof TControl)
+ return $control->getText();
+ }
+
+ protected function createMultiLineControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $control = new TTextBox();
+ $control->setText($value);
+ $control->setTextMode(TTextBoxMode::MultiLine);
+ $control->setCssClass('multiline-textbox scaffold_input');
+ $this->setNotNullProperty($container, $control, $column, $record);
+ return $control;
+ }
+
+ protected function createYearControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $control = new TDropDownList();
+ $years = array();
+ $current = intval(@date('Y'));
+ $from = $current-10; $to=$current+10;
+ for($i = $from; $i <= $to; $i++)
+ $years[$i] = $i;
+ $control->setDataSource($years);
+ $control->setSelectedValue(empty($value) ? $current : $value);
+ $control->setCssClass('year-dropdown');
+ $this->setNotNullProperty($container, $control, $column, $record);
+ return $control;
+ }
+
+ protected function createIntegerControl($container, $column, $record)
+ {
+ $control = $this->createDefaultControl($container, $column, $record);
+ $val = $this->createTypeValidator($container, $column, $record);
+ $val->setDataType(TValidationDataType::Integer);
+ $val->setErrorMessage('Please entery an integer.');
+ return $control;
+ }
+
+ protected function createFloatControl($container, $column, $record)
+ {
+ $control = $this->createDefaultControl($container, $column, $record);
+ $val = $this->createTypeValidator($container, $column, $record);
+ $val->setDataType(TValidationDataType::Float);
+ $val->setErrorMessage('Please entery a decimal number.');
+ if(($max= $column->getMaxiumNumericConstraint())!==null)
+ {
+ $val = $this->createRangeValidator($container,$column,$record);
+ $val->setDataType(TValidationDataType::Float);
+ $val->setMaxValue($max);
+ $val->setStrictComparison(true);
+ $val->setErrorMessage('Please entery a decimal number strictly less than '.$max.'.');
+ }
+ return $control;
+ }
+
+ protected function createRequiredValidator($container, $column, $record)
+ {
+ $val = new TRequiredFieldValidator();
+ $val->setErrorMessage('*');
+ $val->setControlCssClass('required-input');
+ $val->setCssClass('required');
+ $val->setControlToValidate(self::DEFAULT_ID);
+ $val->setValidationGroup($this->getParent()->getValidationGroup());
+ $val->setDisplay(TValidatorDisplayStyle::Dynamic);
+ $container->Controls[] = $val;
+ return $val;
+ }
+
+ protected function createTypeValidator($container, $column, $record)
+ {
+ $val = new TDataTypeValidator();
+ $val->setControlCssClass('required-input2');
+ $val->setCssClass('required');
+ $val->setControlToValidate(self::DEFAULT_ID);
+ $val->setValidationGroup($this->getParent()->getValidationGroup());
+ $val->setDisplay(TValidatorDisplayStyle::Dynamic);
+ $container->Controls[] = $val;
+ return $val;
+ }
+
+ protected function createRangeValidator($container, $column, $record)
+ {
+ $val = new TRangeValidator();
+ $val->setControlCssClass('required-input3');
+ $val->setCssClass('required');
+ $val->setControlToValidate(self::DEFAULT_ID);
+ $val->setValidationGroup($this->getParent()->getValidationGroup());
+ $val->setDisplay(TValidatorDisplayStyle::Dynamic);
+ $container->Controls[] = $val;
+ return $val;
+ }
+
+ protected function createTimeControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $hours=array();
+ for($i=0;$i<24;$i++) $hours[] = str_pad($i,2,'0',STR_PAD_LEFT);
+ $mins=array();
+ for($i=0;$i<60;$i++) $mins[] = str_pad($i,2,'0',STR_PAD_LEFT);
+ $hour = intval(@date('H'));
+ $min = intval(@date('i'));
+ $sec = intval(@date('s'));
+ if(!empty($value))
+ {
+ $match=array();
+ if(preg_match('/(\d+):(\d+):?(\d+)?/', $value, $match))
+ {
+ $hour = $match[1];
+ $min = $match[2];
+ if(isset($match[3]))
+ $sec=$match[3];
+ }
+ }
+
+ $hcontrol = new TDropDownList();
+ $hcontrol->setDataSource($hours);
+ $hcontrol->setID(self::DEFAULT_ID);
+ $hcontrol->dataBind();
+ $hcontrol->setSelectedValue(intval($hour));
+ $container->Controls[] = $hcontrol;
+ $container->Controls[] = ' : ';
+
+ $mcontrol = new TDropDownList();
+ $mcontrol->setDataSource($mins);
+ $mcontrol->dataBind();
+ $mcontrol->setID('scaffold_time_min');
+ $mcontrol->setSelectedValue(intval($min));
+ $container->Controls[] = $mcontrol;
+ $container->Controls[] = ' : ';
+
+ $scontrol = new TDropDownList();
+ $scontrol->setDataSource($mins);
+ $scontrol->dataBind();
+ $scontrol->setID('scaffold_time_sec');
+ $scontrol->setSelectedValue(intval($sec));
+ $container->Controls[] = $scontrol;
+
+ return array($hcontrol,$mcontrol,$scontrol);
+ }
+
+
+ protected function createDateControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $control = new TDatePicker();
+ $control->setFromYear(1900);
+ $control->setInputMode(TDatePickerInputMode::DropDownList);
+ $control->setDateFormat('yyyy-MM-dd');
+ if(!empty($value))
+ $control->setDate(substr($value,0,10));
+ $control->setCssClass('date-dropdown');
+ $this->setNotNullProperty($container, $control, $column, $record);
+ return $control;
+ }
+
+ protected function createDateTimeControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $control = $this->createDateControl($container, $column, $record);
+ $container->Controls[] = ' @ ';
+ $time = $this->createTimeControl($container, $column, $record);
+ if(!empty($value))
+ {
+ $match=array();
+ if(preg_match('/(\d+):(\d+):?(\d+)?/', substr($value, 11), $match))
+ {
+ $time[0]->setSelectedValue(intval($match[1]));
+ $time[1]->setSelectedValue(intval($match[2]));
+ if(isset($match[3]))
+ $time[2]->setSelectedValue(intval($match[3]));
+ }
+ }
+ $time[0]->setID('scaffold_time_hour');
+ return array($control, $time[0], $time[1], $time[2]);
+ }
+
+ protected function getDateTimeValue($container, $column, $record)
+ {
+ $date = $container->findControl(self::DEFAULT_ID)->getDate();
+ $hour = $container->findControl('scaffold_time_hour')->getSelectedValue();
+ $mins = $container->findControl('scaffold_time_min')->getSelectedValue();
+ $secs = $container->findControl('scaffold_time_sec')->getSelectedValue();
+ return "{$date} {$hour}:{$mins}:{$secs}";
+ }
+
+ protected function getTimeValue($container, $column, $record)
+ {
+ $hour = $container->findControl(self::DEFAULT_ID)->getSelectedValue();
+ $mins = $container->findControl('scaffold_time_min')->getSelectedValue();
+ $secs = $container->findControl('scaffold_time_sec')->getSelectedValue();
+ return "{$hour}:{$mins}:{$secs}";
+ }
+
+ protected function createSetControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $selectedValues = preg_split('/\s*,\s*/', $value);
+ $control = new TCheckBoxList();
+ $values = $column->getDbTypeValues();
+ $control->setDataSource($values);
+ $control->dataBind();
+ $control->setSelectedIndices($this->getMatchingIndices($values,$selectedValues));
+ $control->setID(self::DEFAULT_ID);
+ $control->setCssClass('set-checkboxes');
+ $this->setNotNullProperty($container, $control, $column, $record);
+ return $control;
+ }
+
+ protected function getMatchingIndices($checks, $values)
+ {
+ $index=array();
+ for($i=0, $k=count($checks); $i<$k; $i++)
+ {
+ if(in_array($checks[$i], $values))
+ $index[] = $i;
+ }
+ return $index;
+ }
+
+ protected function createEnumControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $selectedValues = preg_split('/\s*,\s*/', $value);
+ $control = new TRadioButtonList();
+ $values = $column->getDbTypeValues();
+ $control->setDataSource($values);
+ $control->dataBind();
+ $index = $this->getMatchingIndices($values,$selectedValues);
+ if(count($index) > 0)
+ $control->setSelectedIndex($index[0]);
+ $control->setID(self::DEFAULT_ID);
+ $control->setCssClass('enum-radio-buttons');
+ $this->setNotNullProperty($container, $control, $column, $record);
+ return $control;
+ }
+
+ protected function getSetValue($container, $column, $record)
+ {
+ $value=array();
+ foreach($container->findControl(self::DEFAULT_ID)->getItems() as $item)
+ {
+ if($item->getSelected())
+ $value[] = $item->getText();
+ }
+ return implode(',', $value);
+ }
+
+ protected function getEnumValue($container, $column, $record)
+ {
+ return $container->findControl(self::DEFAULT_ID)->getSelectedItem()->getText();
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TSqliteScaffoldInput.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TSqliteScaffoldInput.php
new file mode 100644
index 0000000..95078be
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/InputBuilder/TSqliteScaffoldInput.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * TSqliteScaffoldInput class file.
+ *
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold.InputBuilder
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.InputBuilder.TScaffoldInputCommon');
+
+class TSqliteScaffoldInput extends TScaffoldInputCommon
+{
+ protected function createControl($container, $column, $record)
+ {
+ switch(strtolower($column->getDbType()))
+ {
+ case 'boolean':
+ return $this->createBooleanControl($container, $column, $record);
+ case 'date':
+ return $this->createDateControl($container, $column, $record);
+ case 'blob': case 'tinyblob': case 'mediumblob': case 'longblob':
+ case 'text': case 'tinytext': case 'mediumtext': case 'longtext':
+ return $this->createMultiLineControl($container, $column, $record);
+ case 'year':
+ return $this->createYearControl($container, $column, $record);
+ case 'int': case 'integer': case 'tinyint': case 'smallint': case 'mediumint': case 'bigint':
+ return $this->createIntegerControl($container, $column, $record);
+ case 'decimal': case 'double': case 'float':
+ return $this->createFloatControl($container, $column, $record);
+ case 'time' :
+ return $this->createTimeControl($container, $column, $record);
+ case 'datetime': case 'timestamp':
+ return $this->createDateTimeControl($container, $column, $record);
+ default:
+ return $this->createDefaultControl($container,$column, $record);
+ }
+ }
+
+ protected function getControlValue($container, $column, $record)
+ {
+ switch(strtolower($column->getDbType()))
+ {
+ case 'boolean':
+ return $container->findControl(self::DEFAULT_ID)->getChecked();
+ case 'date':
+ return $container->findControl(self::DEFAULT_ID)->getDate();
+ case 'year':
+ return $container->findControl(self::DEFAULT_ID)->getSelectedValue();
+ case 'time':
+ return $this->getTimeValue($container, $column, $record);
+ case 'datetime': case 'timestamp':
+ return $this->getDateTimeValue($container,$column, $record);
+ default:
+ return $this->getDefaultControlValue($container,$column, $record);
+ }
+ }
+
+ protected function createDateControl($container, $column, $record)
+ {
+ $control = parent::createDateControl($container, $column, $record);
+ $value = $this->getRecordPropertyValue($column, $record);
+ if(!empty($value) && preg_match('/timestamp/i', $column->getDbType()))
+ $control->setTimestamp(intval($value));
+ return $control;
+ }
+
+ protected function createDateTimeControl($container, $column, $record)
+ {
+ $value = $this->getRecordPropertyValue($column, $record);
+ $time = parent::createDateTimeControl($container, $column, $record);
+ if(!empty($value) && preg_match('/timestamp/i', $column->getDbType()))
+ {
+ $s = Prado::createComponent('System.Util.TDateTimeStamp');
+ $date = $s->getDate(intval($value));
+ $time[1]->setSelectedValue($date['hours']);
+ $time[2]->setSelectedValue($date['minutes']);
+ $time[3]->setSelectedValue($date['seconds']);
+ }
+ return $time;
+ }
+
+ protected function getDateTimeValue($container, $column, $record)
+ {
+ if(preg_match('/timestamp/i', $column->getDbType()))
+ {
+ $time = $container->findControl(self::DEFAULT_ID)->getTimestamp();
+ $s = Prado::createComponent('System.Util.TDateTimeStamp');
+ $date = $s->getDate($time);
+ $hour = $container->findControl('scaffold_time_hour')->getSelectedValue();
+ $mins = $container->findControl('scaffold_time_min')->getSelectedValue();
+ $secs = $container->findControl('scaffold_time_sec')->getSelectedValue();
+ return $s->getTimeStamp($hour,$mins,$secs,$date['mon'],$date['mday'],$date['year']);
+ }
+ else
+ return parent::getDateTimeValue($container, $column, $record);
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldBase.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldBase.php
new file mode 100644
index 0000000..15cb2c0
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldBase.php
@@ -0,0 +1,205 @@
+<?php
+/**
+ * TScaffoldBase class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold
+ */
+
+/**
+ * Include the base Active Record class.
+ */
+Prado::using('System.Data.ActiveRecord.TActiveRecord');
+
+/**
+ * Base class for Active Record scaffold views.
+ *
+ * Provides common properties for all scaffold views (such as, TScaffoldListView,
+ * TScaffoldEditView, TScaffoldListView and TScaffoldView).
+ *
+ * During the OnPrRender stage the default css style file (filename style.css)
+ * is published and registered. To override the default style, provide your own stylesheet
+ * file explicitly.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord.Scaffold
+ * @since 3.1
+ */
+abstract class TScaffoldBase extends TTemplateControl
+{
+ /**
+ * @var TActiveRecord record instance (may be new or retrieved from db)
+ */
+ private $_record;
+
+ /**
+ * @return TDbMetaData table/view information
+ */
+ protected function getTableInfo()
+ {
+ $finder = $this->getRecordFinder();
+ $gateway = $finder->getRecordManager()->getRecordGateWay();
+ return $gateway->getRecordTableInfo($finder);
+ }
+
+ /**
+ * @param TActiveRecord record instance
+ * @return array record property values
+ */
+ protected function getRecordPropertyValues($record)
+ {
+ $data = array();
+ foreach($this->getTableInfo()->getColumns() as $name=>$column)
+ $data[] = $record->getColumnValue($name);
+ return $data;
+ }
+
+ /**
+ * @param TActiveRecord record instance
+ * @return array record primary key values.
+ */
+ protected function getRecordPkValues($record)
+ {
+ $data=array();
+ foreach($this->getTableInfo()->getColumns() as $name=>$column)
+ {
+ if($column->getIsPrimaryKey())
+ $data[] = $record->getColumnValue($name);
+ }
+ return $data;
+ }
+
+ /**
+ * Name of the Active Record class to be viewed or scaffolded.
+ * @return string Active Record class name.
+ */
+ public function getRecordClass()
+ {
+ return $this->getViewState('RecordClass');
+ }
+
+ /**
+ * Name of the Active Record class to be viewed or scaffolded.
+ * @param string Active Record class name.
+ */
+ public function setRecordClass($value)
+ {
+ $this->setViewState('RecordClass', $value);
+ }
+
+ /**
+ * Copy the view details from another scaffold view instance.
+ * @param TScaffoldBase scaffold view.
+ */
+ protected function copyFrom(TScaffoldBase $obj)
+ {
+ $this->_record = $obj->_record;
+ $this->setRecordClass($obj->getRecordClass());
+ $this->setEnableDefaultStyle($obj->getEnableDefaultStyle());
+ }
+
+ /**
+ * Unset the current record instance and table information.
+ */
+ protected function clearRecordObject()
+ {
+ $this->_record=null;
+ }
+
+ /**
+ * Gets the current Active Record instance. Creates new instance if the
+ * primary key value is null otherwise the record is fetched from the db.
+ * @param array primary key value
+ * @return TActiveRecord record instance
+ */
+ protected function getRecordObject($pk=null)
+ {
+ if($this->_record===null)
+ {
+ if($pk!==null)
+ {
+ $this->_record=$this->getRecordFinder()->findByPk($pk);
+ if($this->_record===null)
+ throw new TConfigurationException('scaffold_invalid_record_pk',
+ $this->getRecordClass(), $pk);
+ }
+ else
+ {
+ $class = $this->getRecordClass();
+ if($class!==null)
+ $this->_record=Prado::createComponent($class);
+ else
+ {
+ throw new TConfigurationException('scaffold_invalid_record_class',
+ $this->getRecordClass(),$this->getID());
+ }
+ }
+ }
+ return $this->_record;
+ }
+
+ /**
+ * @param TActiveRecord Active Record instance.
+ */
+ protected function setRecordObject(TActiveRecord $value)
+ {
+ $this->_record=$value;
+ }
+
+ /**
+ * @return TActiveRecord Active Record finder instance
+ */
+ protected function getRecordFinder()
+ {
+ return TActiveRecord::finder($this->getRecordClass());
+ }
+
+ /**
+ * @return string default scaffold stylesheet name
+ */
+ public function getDefaultStyle()
+ {
+ return $this->getViewState('DefaultStyle', 'style');
+ }
+
+ /**
+ * @param string default scaffold stylesheet name
+ */
+ public function setDefaultStyle($value)
+ {
+ $this->setViewState('DefaultStyle', TPropertyValue::ensureString($value), 'style');
+ }
+
+ /**
+ * @return boolean enable default stylesheet, default is true.
+ */
+ public function getEnableDefaultStyle()
+ {
+ return $this->getViewState('EnableDefaultStyle', true);
+ }
+
+ /**
+ * @param boolean enable default stylesheet, default is true.
+ */
+ public function setEnableDefaultStyle($value)
+ {
+ return $this->setViewState('EnableDefaultStyle', TPropertyValue::ensureBoolean($value), true);
+ }
+
+ /**
+ * Publish the default stylesheet file.
+ */
+ public function onPreRender($param)
+ {
+ parent::onPreRender($param);
+ if($this->getEnableDefaultStyle())
+ {
+ $url = $this->publishAsset($this->getDefaultStyle().'.css');
+ $this->getPage()->getClientScript()->registerStyleSheetFile($url,$url);
+ }
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldEditView.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldEditView.php
new file mode 100644
index 0000000..1d706d6
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldEditView.php
@@ -0,0 +1,306 @@
+<?php
+/**
+ * TScaffoldEditView class and IScaffoldEditRenderer interface file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold
+ */
+
+/**
+ * Load scaffold base.
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.TScaffoldBase');
+
+/**
+ * Template control for editing an Active Record instance.
+ * The <b>RecordClass</b> determines the Active Record class to be edited.
+ * A particular record can be edited by specifying the {@link setRecordPk RecordPk}
+ * value (may be an array for composite keys).
+ *
+ * The default editor input controls are created based on the column types.
+ * The editor layout can be specified by a renderer by set the value
+ * of the {@link setEditRenderer EditRenderer} property to the class name of a
+ * class that implements TScaffoldEditRenderer. A renderer is an external
+ * template control that implements IScaffoldEditRenderer.
+ *
+ * The <b>Data</b> of the IScaffoldEditRenderer will be set as the current Active
+ * Record to be edited. The <b>UpdateRecord()</b> method of IScaffoldEditRenderer
+ * is called when request to save the record is requested.
+ *
+ * Validators in the custom external editor template should have the
+ * {@link TBaseValidator::setValidationGroup ValidationGroup} property set to the
+ * value of the {@link getValidationGroup} of the TScaffoldEditView instance
+ * (the edit view instance is the <b>Parent</b> of the IScaffoldEditRenderer in most
+ * cases.
+ *
+ * Cosmetic changes to the default editor should be done using Cascading Stylesheets.
+ * For example, a particular field/property can be hidden by specifying "display:none" for
+ * the corresponding style (each field/property has unique Css class name as "property_xxx", where
+ * xxx is the property name).
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord.Scaffold
+ * @since 3.1
+ */
+class TScaffoldEditView extends TScaffoldBase
+{
+ /**
+ * @var IScaffoldEditRenderer custom scaffold edit renderer
+ */
+ private $_editRenderer;
+
+ /**
+ * Initialize the editor form if it is Visible.
+ */
+ public function onLoad($param)
+ {
+ if($this->getVisible())
+ $this->initializeEditForm();
+ }
+
+ /**
+ * @return string the class name for scaffold editor. Defaults to empty, meaning not set.
+ */
+ public function getEditRenderer()
+ {
+ return $this->getViewState('EditRenderer', '');
+ }
+
+ /**
+ * @param string the class name for scaffold editor. Defaults to empty, meaning not set.
+ */
+ public function setEditRenderer($value)
+ {
+ $this->setViewState('EditRenderer', $value, '');
+ }
+
+ /**
+ * @param array Active Record primary key value to be edited.
+ */
+ public function setRecordPk($value)
+ {
+ $this->clearRecordObject();
+ $val = TPropertyValue::ensureArray($value);
+ $this->setViewState('PK', count($val) > 0 ? $val : null);
+ }
+
+ /**
+ * @return array Active Record primary key value.
+ */
+ public function getRecordPk()
+ {
+ return $this->getViewState('PK');
+ }
+
+ /**
+ * @return TActiveRecord current Active Record instance
+ */
+ protected function getCurrentRecord()
+ {
+ return $this->getRecordObject($this->getRecordPk());
+ }
+
+ /**
+ * Initialize the editor form
+ */
+ public function initializeEditForm()
+ {
+ $record = $this->getCurrentRecord();
+ $classPath = $this->getEditRenderer();
+ if($classPath === '')
+ {
+ $columns = $this->getTableInfo()->getColumns();
+ $this->getInputRepeater()->setDataSource($columns);
+ $this->getInputRepeater()->dataBind();
+ }
+ else
+ {
+ if($this->_editRenderer===null)
+ $this->createEditRenderer($record, $classPath);
+ else
+ $this->_editRenderer->setData($record);
+ }
+ }
+
+ /**
+ * Instantiate the external edit renderer.
+ * @param TActiveRecord record to be edited
+ * @param string external edit renderer class name.
+ * @throws TConfigurationException raised when renderer is not an
+ * instance of IScaffoldEditRenderer.
+ */
+ protected function createEditRenderer($record, $classPath)
+ {
+ $this->_editRenderer = Prado::createComponent($classPath);
+ if($this->_editRenderer instanceof IScaffoldEditRenderer)
+ {
+ $index = $this->getControls()->remove($this->getInputRepeater());
+ $this->getControls()->insertAt($index,$this->_editRenderer);
+ $this->_editRenderer->setData($record);
+ }
+ else
+ {
+ throw new TConfigurationException(
+ 'scaffold_invalid_edit_renderer', $this->getID(), get_class($record));
+ }
+ }
+
+ /**
+ * Initialize the default editor using the scaffold input builder.
+ */
+ protected function createRepeaterEditItem($sender, $param)
+ {
+ $type = $param->getItem()->getItemType();
+ if($type==TListItemType::Item || $type==TListItemType::AlternatingItem)
+ {
+ $item = $param->getItem();
+ $column = $item->getDataItem();
+ if($column===null)
+ return;
+
+ $record = $this->getCurrentRecord();
+ $builder = $this->getScaffoldInputBuilder($record);
+ $builder->createScaffoldInput($this, $item, $column, $record);
+ }
+ }
+
+ /**
+ * Bubble the command name event. Stops bubbling when the page validator false.
+ * Otherwise, the bubble event is continued.
+ */
+ public function bubbleEvent($sender, $param)
+ {
+ switch(strtolower($param->getCommandName()))
+ {
+ case 'save':
+ return $this->doSave() ? false : true;
+ case 'clear':
+ $this->setRecordPk(null);
+ $this->initializeEditForm();
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check the validators, then tries to save the record.
+ * @return boolean true if the validators are true, false otherwise.
+ */
+ protected function doSave()
+ {
+ if($this->getPage()->getIsValid())
+ {
+ $record = $this->getCurrentRecord();
+ if($this->_editRenderer===null)
+ {
+ $table = $this->getTableInfo();
+ $builder = $this->getScaffoldInputBuilder($record);
+ foreach($this->getInputRepeater()->getItems() as $item)
+ {
+ $column = $table->getColumn($item->getCustomData());
+ $builder->loadScaffoldInput($this, $item, $column, $record);
+ }
+ }
+ else
+ {
+ $this->_editRenderer->updateRecord($record);
+ }
+ $record->save();
+ return true;
+ }
+ else if($this->_editRenderer!==null)
+ {
+ //preserve the form data.
+ $this->_editRenderer->updateRecord($this->getCurrentRecord());
+ }
+
+ return false;
+ }
+
+ /**
+ * @return TRepeater default editor input controls repeater
+ */
+ protected function getInputRepeater()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_repeater');
+ }
+
+ /**
+ * @return TButton Button triggered to save the Active Record.
+ */
+ public function getSaveButton()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_save');
+ }
+
+ /**
+ * @return TButton Button to clear the editor inputs.
+ */
+ public function getClearButton()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_clear');
+ }
+
+ /**
+ * @return TButton Button to cancel the edit action (e.g. hide the edit view).
+ */
+ public function getCancelButton()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_cancel');
+ }
+
+ /**
+ * Create the default scaffold editor control factory.
+ * @param TActiveRecord record instance.
+ * @return TScaffoldInputBase scaffold editor control factory.
+ */
+ protected function getScaffoldInputBuilder($record)
+ {
+ static $_builders=array();
+ $class = get_class($record);
+ if(!isset($_builders[$class]))
+ {
+ Prado::using('System.Data.ActiveRecord.Scaffold.InputBuilder.TScaffoldInputBase');
+ $_builders[$class] = TScaffoldInputBase::createInputBuilder($record);
+ }
+ return $_builders[$class];
+ }
+
+ /**
+ * @return string editor validation group name.
+ */
+ public function getValidationGroup()
+ {
+ return 'group_'.$this->getUniqueID();
+ }
+}
+
+/**
+ * IScaffoldEditRenderer interface.
+ *
+ * IScaffoldEditRenderer defines the interface that an edit renderer
+ * needs to implement. Besides the {@link getData Data} property, an edit
+ * renderer also needs to provide {@link updateRecord updateRecord} method
+ * that is called before the save() method is called on the TActiveRecord.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord.Scaffold
+ * @since 3.1
+ */
+interface IScaffoldEditRenderer extends IDataRenderer
+{
+ /**
+ * This method should update the record with the user input data.
+ * @param TActiveRecord record to be saved.
+ */
+ public function updateRecord($record);
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldEditView.tpl b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldEditView.tpl
new file mode 100644
index 0000000..b3289c0
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldEditView.tpl
@@ -0,0 +1,21 @@
+<div class="scaffold_edit_view">
+<div class="edit-inputs">
+<com:TRepeater ID="_repeater" onItemCreated="createRepeaterEditItem">
+ <prop:ItemTemplate>
+ <div class="edit-item item_<%# $this->ItemIndex % 2 %>
+ input_<%# $this->ItemIndex %> property_<%# $this->DataItem->ColumnId %>">
+ <com:TLabel ID="_label" CssClass="item-label"/>
+ <span class="item-input">
+ <com:TPlaceHolder ID="_input" />
+ </span>
+ </div>
+ </prop:ItemTemplate>
+</com:TRepeater>
+</div>
+
+<div class="edit-page-buttons">
+<com:TButton ID="_save" Text="Save" CommandName="save" ValidationGroup=<%= $this->ValidationGroup %>/>
+<com:TButton ID="_clear" Text="Clear" CommandName="clear" CausesValidation="false"/>
+<com:TButton ID="_cancel" Text="Cancel" CommandName="cancel" CausesValidation="false" Visible="false"/>
+</div>
+</div> \ No newline at end of file
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldListView.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldListView.php
new file mode 100644
index 0000000..d5367e9
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldListView.php
@@ -0,0 +1,304 @@
+<?php
+/**
+ * TScaffoldListView class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold
+ */
+
+/**
+ * Load the scaffold base class.
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.TScaffoldBase');
+
+/**
+ * TScaffoldListView displays a list of Active Records.
+ *
+ * The {@link getHeader Header} property is a TRepeater displaying the
+ * Active Record property/field names. The {@link getSort Sort} property
+ * is a drop down list displaying the combination of properties and its possible
+ * ordering. The {@link getPager Pager} property is a TPager control displaying
+ * the links and/or buttons that navigate to different pages in the Active Record data.
+ * The {@link getList List} property is a TRepeater that renders a row of
+ * Active Record data.
+ *
+ * Custom rendering of the each Active Record can be achieved by specifying
+ * the ItemTemplate or AlternatingItemTemplate property of the main {@linnk getList List}
+ * repeater.
+ *
+ * The TScaffoldListView will listen for two command events named "delete" and
+ * "edit". A "delete" command will delete a the record for the row where the
+ * "delete" command is originates. An "edit" command will push
+ * the record data to be edited by a TScaffoldEditView with ID specified by the
+ * {@link setEditViewID EditViewID}.
+ *
+ * Additional {@link setSearchCondition SearchCondition} and
+ * {@link setSearchParameters SearchParameters} (takes array values) can be
+ * specified to customize the records to be shown. The {@link setSearchCondition SearchCondition}
+ * will be used as the Condition property of TActiveRecordCriteria, and similarly
+ * the {@link setSearchParameters SearchParameters} will be the corresponding
+ * Parameters property of TActiveRecordCriteria.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord.Scaffold
+ * @since 3.1
+ */
+class TScaffoldListView extends TScaffoldBase
+{
+ /**
+ * Initialize the sort drop down list and the column names repeater.
+ */
+ protected function initializeSort()
+ {
+ $table = $this->getTableInfo();
+ $sorts = array('Sort By', str_repeat('-',15));
+ $headers = array();
+ foreach($table->getColumns() as $name=>$colum)
+ {
+ $fname = ucwords(str_replace('_', ' ', $name));
+ $sorts[$name.' ASC'] = $fname .' Ascending';
+ $sorts[$name.' DESC'] = $fname .' Descending';
+ $headers[] = $fname ;
+ }
+ $this->_sort->setDataSource($sorts);
+ $this->_sort->dataBind();
+ $this->_header->setDataSource($headers);
+ $this->_header->dataBind();
+ }
+
+ /**
+ * Loads and display the data.
+ */
+ public function onPreRender($param)
+ {
+ parent::onPreRender($param);
+ if(!$this->getPage()->getIsPostBack() || $this->getViewState('CurrentClass')!=$this->getRecordClass())
+ {
+ $this->initializeSort();
+ $this->setViewState('CurrentClass', $this->getRecordClass());
+ }
+ $this->loadRecordData();
+ }
+
+ /**
+ * Fetch the records and data bind it to the list.
+ */
+ protected function loadRecordData()
+ {
+ $search = new TActiveRecordCriteria($this->getSearchCondition(), $this->getSearchParameters());
+ $this->_list->setVirtualItemCount($this->getRecordFinder()->count($search));
+ $finder = $this->getRecordFinder();
+ $criteria = $this->getRecordCriteria();
+ $this->_list->setDataSource($finder->findAll($criteria));
+ $this->_list->dataBind();
+ }
+
+ /**
+ * @return TActiveRecordCriteria sort/search/paging criteria
+ */
+ protected function getRecordCriteria()
+ {
+ $total = $this->_list->getVirtualItemCount();
+ $limit = $this->_list->getPageSize();
+ $offset = $this->_list->getCurrentPageIndex()*$limit;
+ if($offset + $limit > $total)
+ $limit = $total - $offset;
+ $criteria = new TActiveRecordCriteria($this->getSearchCondition(), $this->getSearchParameters());
+ if($limit > 0)
+ {
+ $criteria->setLimit($limit);
+ if($offset <= $total)
+ $criteria->setOffset($offset);
+ }
+ $order = explode(' ',$this->_sort->getSelectedValue(), 2);
+ if(is_array($order) && count($order) === 2)
+ $criteria->OrdersBy[$order[0]] = $order[1];
+ return $criteria;
+ }
+
+ /**
+ * @param string search condition, the SQL string after the WHERE clause.
+ */
+ public function setSearchCondition($value)
+ {
+ $this->setViewState('SearchCondition', $value);
+ }
+
+ /**
+ * @param string SQL search condition for list display.
+ */
+ public function getSearchCondition()
+ {
+ return $this->getViewState('SearchCondition');
+ }
+
+ /**
+ * @param array search parameters
+ */
+ public function setSearchParameters($value)
+ {
+ $this->setViewState('SearchParameters', TPropertyValue::ensureArray($value),array());
+ }
+
+ /**
+ * @return array search parameters
+ */
+ public function getSearchParameters()
+ {
+ return $this->getViewState('SearchParameters', array());
+ }
+
+ /**
+ * Continue bubbling the "edit" command, "delete" command is handled in this class.
+ */
+ public function bubbleEvent($sender, $param)
+ {
+ switch(strtolower($param->getCommandName()))
+ {
+ case 'delete':
+ return $this->deleteRecord($sender, $param);
+ case 'edit':
+ $this->initializeEdit($sender, $param);
+ }
+ $this->raiseBubbleEvent($this, $param);
+ return true;
+ }
+
+ /**
+ * Initialize the edit view control form when EditViewID is set.
+ */
+ protected function initializeEdit($sender, $param)
+ {
+ if(($ctrl=$this->getEditViewControl())!==null)
+ {
+ if($param instanceof TRepeaterCommandEventParameter)
+ {
+ $pk = $param->getItem()->getCustomData();
+ $ctrl->setRecordPk($pk);
+ $ctrl->initializeEditForm();
+ }
+ }
+ }
+
+ /**
+ * Deletes an Active Record.
+ */
+ protected function deleteRecord($sender, $param)
+ {
+ if($param instanceof TRepeaterCommandEventParameter)
+ {
+ $pk = $param->getItem()->getCustomData();
+ $this->getRecordFinder()->deleteByPk($pk);
+ }
+ }
+
+ /**
+ * Initialize the default display for each Active Record item.
+ */
+ protected function listItemCreated($sender, $param)
+ {
+ $item = $param->getItem();
+ if($item instanceof IItemDataRenderer)
+ {
+ $type = $item->getItemType();
+ if($type==TListItemType::Item || $type==TListItemType::AlternatingItem)
+ $this->populateField($sender, $param);
+ }
+ }
+
+ /**
+ * Sets the Record primary key to the current repeater item's CustomData.
+ * Binds the inner repeater with properties of the current Active Record.
+ */
+ protected function populateField($sender, $param)
+ {
+ $item = $param->getItem();
+ if(($data = $item->getData()) !== null)
+ {
+ $item->setCustomData($this->getRecordPkValues($data));
+ if(($prop = $item->findControl('_properties'))!==null)
+ {
+ $item->_properties->setDataSource($this->getRecordPropertyValues($data));
+ $item->_properties->dataBind();
+ }
+ }
+ }
+
+ /**
+ * Updates repeater page index with the pager new index value.
+ */
+ protected function pageChanged($sender, $param)
+ {
+ $this->_list->setCurrentPageIndex($param->getNewPageIndex());
+ }
+
+ /**
+ * @return TRepeater Repeater control for Active Record instances.
+ */
+ public function getList()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_list');
+ }
+
+ /**
+ * @return TPager List pager control.
+ */
+ public function getPager()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_pager');
+ }
+
+ /**
+ * @return TDropDownList Control that displays and controls the record ordering.
+ */
+ public function getSort()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_sort');
+ }
+
+ /**
+ * @return TRepeater Repeater control for record property names.
+ */
+ public function getHeader()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_header');
+ }
+
+ /**
+ * @return string TScaffoldEditView control ID for editing selected Active Record.
+ */
+ public function getEditViewID()
+ {
+ return $this->getViewState('EditViewID');
+ }
+
+ /**
+ * @param string TScaffoldEditView control ID for editing selected Active Record.
+ */
+ public function setEditViewID($value)
+ {
+ $this->setViewState('EditViewID', $value);
+ }
+
+ /**
+ * @return TScaffoldEditView control for editing selected Active Record, null if EditViewID is not set.
+ */
+ protected function getEditViewControl()
+ {
+ if(($id=$this->getEditViewID())!==null)
+ {
+ $ctrl = $this->getParent()->findControl($id);
+ if($ctrl===null)
+ throw new TConfigurationException('scaffold_unable_to_find_edit_view', $id);
+ return $ctrl;
+ }
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldListView.tpl b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldListView.tpl
new file mode 100644
index 0000000..c70e864
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldListView.tpl
@@ -0,0 +1,57 @@
+<div class="scaffold_list_view">
+<div class="item-header">
+<com:TRepeater ID="_header">
+ <prop:ItemTemplate>
+ <com:TLabel Text=<%# $this->DataItem %> CssClass="field field_<%# $this->ItemIndex %>"/>
+ </prop:ItemTemplate>
+</com:TRepeater>
+
+<span class="sort-options">
+ <com:TDropDownList ID="_sort" AutoPostBack="true"/>
+</span>
+
+</div>
+
+<div class="item-list">
+<com:TRepeater ID="_list"
+ AllowPaging="true"
+ AllowCustomPaging="true"
+ onItemCommand="bubbleEvent"
+ onItemCreated="listItemCreated"
+ PageSize="10">
+ <prop:ItemTemplate>
+ <div class="item item_<%# $this->ItemIndex % 2 %>">
+
+ <com:TRepeater ID="_properties">
+ <prop:ItemTemplate>
+ <span class="field field_<%# $this->ItemIndex %>">
+ <%# htmlspecialchars($this->DataItem) %>
+ </span>
+ </prop:ItemTemplate>
+ </com:TRepeater>
+
+ <span class="edit-delete-buttons">
+ <com:TButton Text="Edit"
+ Visible=<%# $this->NamingContainer->Parent->EditViewID !== Null %>
+ CommandName="edit"
+ CssClass="edit-button"
+ CausesValidation="false" />
+ <com:TButton Text="Delete"
+ CommandName="delete"
+ CssClass="delete-button"
+ CausesValidation="false"
+ Attributes.onclick="if(!confirm('Are you sure?')) return false;" />
+ </span>
+
+ </div>
+ </prop:ItemTemplate>
+</com:TRepeater>
+</div>
+
+<com:TPager ID="_pager"
+ CssClass="pager"
+ ControlToPaginate="_list"
+ PageButtonCount="10"
+ Mode="Numeric"
+ OnPageIndexChanged="pageChanged" />
+</div> \ No newline at end of file
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldSearch.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldSearch.php
new file mode 100644
index 0000000..e2627e5
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldSearch.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * TScaffoldSearch class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Scaffold
+ */
+
+/**
+ * Import the scaffold base.
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.TScaffoldBase');
+
+/**
+ * TScaffoldSearch provide a simple textbox and a button that is used
+ * to perform search on a TScaffoldListView with ID given by {@link setListViewID ListViewID}.
+ *
+ * The {@link getSearchText SearchText} property is a TTextBox and the
+ * {@link getSearchButton SearchButton} property is a TButton with label value "Search".
+ *
+ * Searchable fields of the Active Record can be restricted by specifying
+ * a comma delimited string of allowable fields in the
+ * {@link setSearchableFields SearchableFields} property. The default is null,
+ * meaning that most text type fields are searched (the default searchable fields
+ * are database dependent).
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.ActiveRecord.Scaffold
+ * @since 3.1
+ */
+class TScaffoldSearch extends TScaffoldBase
+{
+ /**
+ * @var TScaffoldListView the scaffold list view.
+ */
+ private $_list;
+
+ /**
+ * @return TScaffoldListView the scaffold list view this search box belongs to.
+ */
+ protected function getListView()
+ {
+ if($this->_list===null && ($id = $this->getListViewID()) !== null)
+ {
+ $this->_list = $this->getParent()->findControl($id);
+ if($this->_list ===null)
+ throw new TConfigurationException('scaffold_unable_to_find_list_view', $id);
+ }
+ return $this->_list;
+ }
+
+ /**
+ * @param string ID of the TScaffoldListView this search control belongs to.
+ */
+ public function setListViewID($value)
+ {
+ $this->setViewState('ListViewID', $value);
+ }
+
+ /**
+ * @return string ID of the TScaffoldListView this search control belongs to.
+ */
+ public function getListViewID()
+ {
+ return $this->getViewState('ListViewID');
+ }
+
+ /**
+ * Sets the SearchCondition of the TScaffoldListView as the search terms
+ * given by the text of the search text box.
+ */
+ public function bubbleEvent($sender, $param)
+ {
+ if(strtolower($param->getCommandName())==='search')
+ {
+ if(($list = $this->getListView()) !== null)
+ {
+ $list->setSearchCondition($this->createSearchCondition());
+ return false;
+ }
+ }
+ $this->raiseBubbleEvent($this, $param);
+ return true;
+ }
+
+ /**
+ * @return string the search criteria for the search terms in the search text box.
+ */
+ protected function createSearchCondition()
+ {
+ $table = $this->getTableInfo();
+ if(strlen($str=$this->getSearchText()->getText()) > 0)
+ {
+ $builder = $table->createCommandBuilder($this->getRecordFinder()->getDbConnection());
+ return $builder->getSearchExpression($this->getFields(), $str);
+ }
+ }
+
+ /**
+ * @return array list of fields to be searched.
+ */
+ protected function getFields()
+ {
+ if(strlen(trim($str=$this->getSearchableFields()))>0)
+ $fields = preg_split('/\s*,\s*/', $str);
+ else
+ $fields = $this->getTableInfo()->getColumns()->getKeys();
+ return $fields;
+ }
+
+ /**
+ * @return string comma delimited list of fields that may be searched.
+ */
+ public function getSearchableFields()
+ {
+ return $this->getViewState('SearchableFields','');
+ }
+
+ /**
+ * @param string comma delimited list of fields that may be searched.
+ */
+ public function setSearchableFields($value)
+ {
+ $this->setViewState('SearchableFields', $value, '');
+ }
+
+ /**
+ * @return TButton button with default label "Search".
+ */
+ public function getSearchButton()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_search');
+ }
+
+ /**
+ * @return TTextBox search text box.
+ */
+ public function getSearchText()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_textbox');
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldSearch.tpl b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldSearch.tpl
new file mode 100644
index 0000000..a5f56b5
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldSearch.tpl
@@ -0,0 +1,4 @@
+<span class="scaffold_search">
+<com:TTextBox ID="_textbox" />
+<com:TButton ID="_search" Text="Search" CommandName="search" />
+</span> \ No newline at end of file
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldView.php b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldView.php
new file mode 100644
index 0000000..3d4019a
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldView.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * TScaffoldView class.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord.Scaffold
+ */
+
+/**
+ * Import scaffold base, list, edit and search controls.
+ */
+Prado::using('System.Data.ActiveRecord.Scaffold.TScaffoldBase');
+Prado::using('System.Data.ActiveRecord.Scaffold.TScaffoldListView');
+Prado::using('System.Data.ActiveRecord.Scaffold.TScaffoldEditView');
+Prado::using('System.Data.ActiveRecord.Scaffold.TScaffoldSearch');
+
+/**
+ * TScaffoldView is a composite control consisting of TScaffoldListView
+ * with a TScaffoldSearch. In addition, it will display a TScaffoldEditView
+ * when an "edit" command is raised from the TScaffoldListView (when the
+ * edit button is clicked). Futher more, the "add" button can be clicked
+ * that shows an empty data TScaffoldListView for creating new records.
+ *
+ * The {@link getListView ListView} property gives a TScaffoldListView for
+ * display the record data. The {@link getEditView EditView} is the
+ * TScaffoldEditView that renders the
+ * inputs for editing and adding records. The {@link getSearchControl SearchControl}
+ * is a TScaffoldSearch responsible to the search user interface.
+ *
+ * Set the {@link setRecordClass RecordClass} property to the name of
+ * the Active Record class to be displayed/edited/added.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord.Scaffold
+ * @since 3.0
+ */
+class TScaffoldView extends TScaffoldBase
+{
+ /**
+ * Copy basic record details to the list/edit/search controls.
+ */
+ public function onPreRender($param)
+ {
+ parent::onPreRender($param);
+ $this->getListView()->copyFrom($this);
+ $this->getEditView()->copyFrom($this);
+ $this->getSearchControl()->copyFrom($this);
+ }
+
+ /**
+ * @return TScaffoldListView scaffold list view.
+ */
+ public function getListView()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_listView');
+ }
+
+ /**
+ * @return TScaffoldEditView scaffold edit view.
+ */
+ public function getEditView()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_editView');
+ }
+
+ /**
+ * @return TScaffoldSearch scaffold search textbox and button.
+ */
+ public function getSearchControl()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_search');
+ }
+
+ /**
+ * @return TButton "Add new record" button.
+ */
+ public function getAddButton()
+ {
+ $this->ensureChildControls();
+ return $this->getRegisteredObject('_newButton');
+ }
+
+ /**
+ * Handle the "edit" and "new" commands by displaying the edit view.
+ * Default command shows the list view.
+ */
+ public function bubbleEvent($sender,$param)
+ {
+ switch(strtolower($param->getCommandName()))
+ {
+ case 'edit':
+ return $this->showEditView($sender, $param);
+ case 'new':
+ return $this->showAddView($sender, $param);
+ default:
+ return $this->showListView($sender, $param);
+ }
+ return false;
+ }
+
+ /**
+ * Shows the edit record view.
+ */
+ protected function showEditView($sender, $param)
+ {
+ $this->getListView()->setVisible(false);
+ $this->getEditView()->setVisible(true);
+ $this->_panForNewButton->setVisible(false);
+ $this->_panForSearch->setVisible(false);
+ $this->getEditView()->getCancelButton()->setVisible(true);
+ $this->getEditView()->getClearButton()->setVisible(false);
+ }
+
+ /**
+ * Shows the view for listing the records.
+ */
+ protected function showListView($sender, $param)
+ {
+ $this->getListView()->setVisible(true);
+ $this->getEditView()->setVisible(false);
+ $this->_panForNewButton->setVisible(true);
+ $this->_panForSearch->setVisible(true);
+ }
+
+ /**
+ * Shows the add record view.
+ */
+ protected function showAddView($sender, $param)
+ {
+ $this->getEditView()->setRecordPk(null);
+ $this->getEditView()->initializeEditForm();
+ $this->showEditView($sender, $param);
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldView.tpl b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldView.tpl
new file mode 100644
index 0000000..eea3952
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/TScaffoldView.tpl
@@ -0,0 +1,11 @@
+<div class="scaffold_view">
+<com:TPanel ID="_panForSearch">
+ <com:TScaffoldSearch ID="_search" ListViewID="_listView" />
+</com:TPanel>
+<com:TScaffoldListView ID="_listView" EditViewID="_editView" />
+<com:TPanel ID="_panForNewButton" CssClass="auxilary-button buttons">
+ <com:TButton ID="_newButton" Text="Add new record" CssClass="new-button" CommandName="new" />
+</com:TPanel>
+
+<com:TScaffoldEditView ID="_editView" Visible="false"/>
+</div> \ No newline at end of file
diff --git a/lib/prado/framework/Data/ActiveRecord/Scaffold/style.css b/lib/prado/framework/Data/ActiveRecord/Scaffold/style.css
new file mode 100644
index 0000000..cd34eb7
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/Scaffold/style.css
@@ -0,0 +1,124 @@
+/* $Id: style.css 1866 2007-04-14 05:02:29Z wei $ */
+body
+{
+ font-family: Cambria, Georgia, "Times New Roman", Times, serif;
+}
+
+.pager
+{
+ display: block;
+ border-top: 1px solid #ccc;
+ padding: 1em;
+}
+
+.pager span, .pager a
+{
+ border: 1px solid #ccc;
+ padding: 0.3em 0.7em;
+ font-weight: bold;
+}
+
+.pager a
+{
+ background-color: #E0FFFF;
+ border-color: #87CEFA;
+}
+
+.pager a:hover
+{
+ background-color: White;
+}
+
+
+.item, .item-header
+{
+ border-top: 1px dashed #B0C4DE;
+ padding: 0.5em;
+ clear: both;
+}
+
+.item-header
+{
+ border-top: 0 none;
+ font-weight: bold;
+}
+
+.item_1
+{
+ background-color: #F0F8FF;
+}
+
+.field_0, .field
+{
+ padding: 0.2em;
+ float: left;
+ width: 150px;
+}
+
+.field_0
+{
+ width: 40px;
+ text-align: center;
+}
+
+.auxilary-button
+{
+ padding: 1em;
+}
+
+.edit-inputs label
+{
+ width: 150px;
+ float: left;
+ text-align: right;
+ padding: 0 0.5em;
+ font-weight: bold;
+}
+
+
+.item-input label
+{
+ float: none;
+ font-weight: normal;
+}
+
+
+.edit-page-buttons
+{
+ padding-left: 120px;
+ padding-top: 20px;
+}
+
+.edit-item
+{
+ padding: 0.4em;
+}
+
+.edit-inputs .scaffold_input
+{
+ width: 250px;
+ border: 1px solid Highlight;
+ padding: 0.2em;
+}
+
+.edit-inputs .multiline-textbox
+{
+ width: 500px;
+ height: 100px;
+}
+
+.edit-inputs .input_0 .scaffold_input
+{
+ width: 50px;
+}
+
+.edit-inputs .required
+{
+ font-weight: bold;
+ padding: 0.2em;
+}
+.edit-inputs .required-input, .edit-inputs .required-input2 .required-input3 .required-input4
+{
+ border: 1px solid red;
+ background-color: #FFF5EE;
+} \ No newline at end of file
diff --git a/lib/prado/framework/Data/ActiveRecord/TActiveRecord.php b/lib/prado/framework/Data/ActiveRecord/TActiveRecord.php
new file mode 100644
index 0000000..186af85
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/TActiveRecord.php
@@ -0,0 +1,1107 @@
+<?php
+/**
+ * TActiveRecord, TActiveRecordEventParameter, TActiveRecordInvalidFinderResult class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord
+ */
+
+/**
+ * Load record manager, criteria and relations.
+ */
+Prado::using('System.Data.ActiveRecord.TActiveRecordManager');
+Prado::using('System.Data.ActiveRecord.TActiveRecordCriteria');
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext');
+
+/**
+ * Base class for active records.
+ *
+ * An active record creates an object that wraps a row in a database table
+ * or view, encapsulates the database access, and adds domain logic on that data.
+ *
+ * Active record objects are stateful, this is main difference between the
+ * TActiveRecord implementation and the TTableGateway implementation.
+ *
+ * The essence of an Active Record is an object model of the
+ * domain (e.g. products, items) that incorporates both behavior and
+ * data in which the classes match very closely the record structure of an
+ * underlying database. Each Active Record is responsible for saving and
+ * loading to the database and also for any domain logic that acts on the data.
+ *
+ * The Active Record provides methods that do the following:
+ * 1. Construct an instance of the Active Record from a SQL result set row.
+ * 2. Construct a new instance for later insertion into the table.
+ * 3. Finder methods to wrap commonly used SQL queries and return Active Record objects.
+ * 4. Update the database and insert into it the data in the Active Record.
+ *
+ * Example:
+ * <code>
+ * class UserRecord extends TActiveRecord
+ * {
+ * const TABLE='users'; //optional table name.
+ *
+ * public $username; //corresponds to the fieldname in the table
+ * public $email;
+ *
+ * //returns active record finder instance
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ *
+ * //create a connection and give it to the ActiveRecord manager.
+ * $dsn = 'pgsql:host=localhost;dbname=test';
+ * $conn = new TDbConnection($dsn, 'dbuser','dbpass');
+ * TActiveRecordManager::getInstance()->setDbConnection($conn);
+ *
+ * //load the user record with username (primary key) 'admin'.
+ * $user = UserRecord::finder()->findByPk('admin');
+ * $user->email = 'admin@example.org';
+ * $user->save(); //update the 'admin' record.
+ * </code>
+ *
+ * Since v3.1.1, TActiveRecord starts to support column mapping. The physical
+ * column names (defined in database) can be mapped to logical column names
+ * (defined in active classes as public properties.) To use this feature, declare
+ * a static class variable COLUMN_MAPPING like the following:
+ * <code>
+ * class UserRecord extends TActiveRecord
+ * {
+ * const TABLE='users';
+ * public static $COLUMN_MAPPING=array
+ * (
+ * 'user_id'=>'username',
+ * 'email_address'=>'email',
+ * );
+ * public $username;
+ * public $email;
+ * }
+ * </code>
+ * In the above, the 'users' table consists of 'user_id' and 'email_address' columns,
+ * while the UserRecord class declares 'username' and 'email' properties.
+ * By using column mapping, we can regularize the naming convention of column names
+ * in active record.
+ *
+ * Since v3.1.2, TActiveRecord enhanced its support to access of foreign objects.
+ * By declaring a public static variable RELATIONS like the following, one can access
+ * the corresponding foreign objects easily:
+ * <code>
+ * class UserRecord extends TActiveRecord
+ * {
+ * const TABLE='users';
+ * public static $RELATIONS=array
+ * (
+ * 'department'=>array(self::BELONGS_TO, 'DepartmentRecord', 'department_id'),
+ * 'contacts'=>array(self::HAS_MANY, 'ContactRecord', 'user_id'),
+ * );
+ * }
+ * </code>
+ * In the above, the users table is related with departments table (represented by
+ * DepartmentRecord) and contacts table (represented by ContactRecord). Now, given a UserRecord
+ * instance $user, one can access its department and contacts simply by: $user->department and
+ * $user->contacts. No explicit data fetching is needed. Internally, the foreign objects are
+ * fetched in a lazy way, which avoids unnecessary overhead if the foreign objects are not accessed
+ * at all.
+ *
+ * Since v3.1.2, new events OnInsert, OnUpdate and OnDelete are available.
+ * The event OnInsert, OnUpdate and OnDelete methods are executed before
+ * inserting, updating, and deleting the current record, respectively. You may override
+ * these methods; a TActiveRecordChangeEventParameter parameter is passed to these methods.
+ * The property {@link TActiveRecordChangeEventParameter::setIsValid IsValid} of the parameter
+ * can be set to false to prevent the change action to be executed. This can be used,
+ * for example, to validate the record before the action is executed. For example,
+ * in the following the password property is hashed before a new record is inserted.
+ * <code>
+ * class UserRecord extends TActiveRecord
+ * {
+ * function OnInsert($param)
+ * {
+ * //parent method should be called to raise the event
+ * parent::OnInsert($param);
+ * $this->nounce = md5(time());
+ * $this->password = md5($this->password.$this->nounce);
+ * }
+ * }
+ * </code>
+ *
+ * Since v3.1.3 you can also define a method that returns the table name.
+ * <code>
+ * class UserRecord extends TActiveRecord
+ * {
+ * public function table()
+ * {
+ * return 'users';
+ * }
+ *
+ * }
+ * </code>
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord
+ * @since 3.1
+ */
+abstract class TActiveRecord extends TComponent
+{
+ const BELONGS_TO='BELONGS_TO';
+ const HAS_ONE='HAS_ONE';
+ const HAS_MANY='HAS_MANY';
+ const MANY_TO_MANY='MANY_TO_MANY';
+
+ const STATE_NEW=0;
+ const STATE_LOADED=1;
+ const STATE_DELETED=2;
+
+ /**
+ * @var integer record state: 0 = new, 1 = loaded, 2 = deleted.
+ * @since 3.1.2
+ */
+ protected $_recordState=0; // use protected so that serialization is fine
+
+ /**
+ * This static variable defines the column mapping.
+ * The keys are physical column names as defined in database,
+ * and the values are logical column names as defined as public variable/property names
+ * for the corresponding active record class.
+ * @var array column mapping. Keys: physical column names, values: logical column names.
+ * @since 3.1.1
+ */
+ public static $COLUMN_MAPPING=array();
+ private static $_columnMapping=array();
+
+ /**
+ * This static variable defines the relationships.
+ * The keys are public variable/property names defined in the AR class.
+ * Each value is an array, e.g. array(self::HAS_MANY, 'PlayerRecord').
+ * @var array relationship.
+ * @since 3.1.1
+ */
+ public static $RELATIONS=array();
+ private static $_relations=array();
+
+ /**
+ * @var TDbConnection database connection object.
+ */
+ protected $_connection; // use protected so that serialization is fine
+
+
+ /**
+ * Defaults to 'null'
+ *
+ * @var TActiveRecordInvalidFinderResult
+ * @since 3.1.5
+ */
+ protected $_invalidFinderResult = null; // use protected so that serialization is fine
+
+ /**
+ * Prevent __call() method creating __sleep() when serializing.
+ */
+ public function __sleep()
+ {
+ return array_diff(parent::__sleep(),array("\0*\0_connection"));
+ }
+
+ /**
+ * Prevent __call() method creating __wakeup() when unserializing.
+ */
+ public function __wakeup()
+ {
+ $this->setupColumnMapping();
+ $this->setupRelations();
+ }
+
+ /**
+ * Create a new instance of an active record with given $data. The record
+ * can be saved to the database specified by the $connection object.
+ *
+ * @param array optional name value pair record data.
+ * @param TDbConnection optional database connection this object record use.
+ */
+ public function __construct($data=array(), $connection=null)
+ {
+ if($connection!==null)
+ $this->setDbConnection($connection);
+ $this->setupColumnMapping();
+ $this->setupRelations();
+ if(!empty($data)) //$data may be an object
+ $this->copyFrom($data);
+ }
+
+ /**
+ * Magic method for reading properties.
+ * This method is overriden to provide read access to the foreign objects via
+ * the key names declared in the RELATIONS array.
+ * @param string property name
+ * @return mixed property value.
+ * @since 3.1.2
+ */
+ public function __get($name)
+ {
+ if($this->hasRecordRelation($name) && !$this->canGetProperty($name))
+ {
+ $this->fetchResultsFor($name);
+ return $this->$name;
+ }
+ return parent::__get($name);
+ }
+
+ /**
+ * Magic method for writing properties.
+ * This method is overriden to provide write access to the foreign objects via
+ * the key names declared in the RELATIONS array.
+ * @param string property name
+ * @param mixed property value.
+ * @since 3.1.2
+ */
+ public function __set($name,$value)
+ {
+ if($this->hasRecordRelation($name) && !$this->canSetProperty($name))
+ $this->$name=$value;
+ else
+ parent::__set($name,$value);
+ }
+
+ /**
+ * @since 3.1.1
+ */
+ private function setupColumnMapping()
+ {
+ $className=get_class($this);
+ if(!isset(self::$_columnMapping[$className]))
+ {
+ $class=new ReflectionClass($className);
+ self::$_columnMapping[$className]=$class->getStaticPropertyValue('COLUMN_MAPPING');
+ }
+ }
+
+ /**
+ * @since 3.1.2
+ */
+ private function setupRelations()
+ {
+ $className=get_class($this);
+ if(!isset(self::$_relations[$className]))
+ {
+ $class=new ReflectionClass($className);
+ $relations=array();
+ foreach($class->getStaticPropertyValue('RELATIONS') as $key=>$value)
+ $relations[strtolower($key)]=array($key,$value);
+ self::$_relations[$className]=$relations;
+ }
+ }
+
+ /**
+ * Copies data from an array or another object.
+ * @throws TActiveRecordException if data is not array or not object.
+ */
+ public function copyFrom($data)
+ {
+ if(is_object($data))
+ $data=get_object_vars($data);
+ if(!is_array($data))
+ throw new TActiveRecordException('ar_data_invalid', get_class($this));
+ foreach($data as $name=>$value)
+ $this->setColumnValue($name,$value);
+ }
+
+
+ public static function getActiveDbConnection()
+ {
+ if(($db=self::getRecordManager()->getDbConnection())!==null)
+ $db->setActive(true);
+ return $db;
+ }
+
+ /**
+ * Gets the current Db connection, the connection object is obtained from
+ * the TActiveRecordManager if connection is currently null.
+ * @return TDbConnection current db connection for this object.
+ */
+ public function getDbConnection()
+ {
+ if($this->_connection===null)
+ $this->_connection=self::getActiveDbConnection();
+ return $this->_connection;
+ }
+
+ /**
+ * @param TDbConnection db connection object for this record.
+ */
+ public function setDbConnection($connection)
+ {
+ $this->_connection=$connection;
+ }
+
+ /**
+ * @return TDbTableInfo the meta information of the table associated with this AR class.
+ */
+ public function getRecordTableInfo()
+ {
+ return $this->getRecordGateway()->getRecordTableInfo($this);
+ }
+
+ /**
+ * Compare two records using their primary key values (all column values if
+ * table does not defined primary keys). The default uses simple == for
+ * comparison of their values. Set $strict=true for identity comparison (===).
+ * @param TActiveRecord another record to compare with.
+ * @param boolean true to perform strict identity comparison
+ * @return boolean true if $record equals, false otherwise.
+ */
+ public function equals(TActiveRecord $record, $strict=false)
+ {
+ if($record===null || get_class($this)!==get_class($record))
+ return false;
+ $tableInfo = $this->getRecordTableInfo();
+ $pks = $tableInfo->getPrimaryKeys();
+ $properties = count($pks) > 0 ? $pks : $tableInfo->getColumns()->getKeys();
+ $equals=true;
+ foreach($properties as $prop)
+ {
+ if($strict)
+ $equals = $equals && $this->getColumnValue($prop) === $record->getColumnValue($prop);
+ else
+ $equals = $equals && $this->getColumnValue($prop) == $record->getColumnValue($prop);
+ if(!$equals)
+ return false;
+ }
+ return $equals;
+ }
+
+ /**
+ * Returns the instance of a active record finder for a particular class.
+ * The finder objects are static instances for each ActiveRecord class.
+ * This means that event handlers bound to these finder instances are class wide.
+ * Create a new instance of the ActiveRecord class if you wish to bound the
+ * event handlers to object instance.
+ * @param string active record class name.
+ * @return TActiveRecord active record finder instance.
+ */
+ public static function finder($className=__CLASS__)
+ {
+ static $finders = array();
+ if(!isset($finders[$className]))
+ {
+ $f = Prado::createComponent($className);
+ $finders[$className]=$f;
+ }
+ return $finders[$className];
+ }
+
+ /**
+ * Gets the record manager for this object, the default is to call
+ * TActiveRecordManager::getInstance().
+ * @return TActiveRecordManager default active record manager.
+ */
+ public static function getRecordManager()
+ {
+ return TActiveRecordManager::getInstance();
+ }
+
+ /**
+ * @return TActiveRecordGateway record table gateway.
+ */
+ public function getRecordGateway()
+ {
+ return TActiveRecordManager::getInstance()->getRecordGateway();
+ }
+
+ /**
+ * Saves the current record to the database, insert or update is automatically determined.
+ * @return boolean true if record was saved successfully, false otherwise.
+ */
+ public function save()
+ {
+ $gateway = $this->getRecordGateway();
+ $param = new TActiveRecordChangeEventParameter();
+ if($this->_recordState===self::STATE_NEW)
+ {
+ $this->onInsert($param);
+ if($param->getIsValid() && $gateway->insert($this))
+ {
+ $this->_recordState = self::STATE_LOADED;
+ return true;
+ }
+ }
+ else if($this->_recordState===self::STATE_LOADED)
+ {
+ $this->onUpdate($param);
+ if($param->getIsValid() && $gateway->update($this))
+ return true;
+ }
+ else
+ throw new TActiveRecordException('ar_save_invalid', get_class($this));
+
+ return false;
+ }
+
+ /**
+ * Deletes the current record from the database. Once deleted, this object
+ * can not be saved again in the same instance.
+ * @return boolean true if the record was deleted successfully, false otherwise.
+ */
+ public function delete()
+ {
+ if($this->_recordState===self::STATE_LOADED)
+ {
+ $gateway = $this->getRecordGateway();
+ $param = new TActiveRecordChangeEventParameter();
+ $this->onDelete($param);
+ if($param->getIsValid() && $gateway->delete($this))
+ {
+ $this->_recordState=self::STATE_DELETED;
+ return true;
+ }
+ }
+ else
+ throw new TActiveRecordException('ar_delete_invalid', get_class($this));
+
+ return false;
+ }
+
+ /**
+ * Delete records by primary key. Usage:
+ *
+ * <code>
+ * $finder->deleteByPk($primaryKey); //delete 1 record
+ * $finder->deleteByPk($key1,$key2,...); //delete multiple records
+ * $finder->deleteByPk(array($key1,$key2,...)); //delete multiple records
+ * </code>
+ *
+ * For composite primary keys (determined from the table definitions):
+ * <code>
+ * $finder->deleteByPk(array($key1,$key2)); //delete 1 record
+ *
+ * //delete multiple records
+ * $finder->deleteByPk(array($key1,$key2), array($key3,$key4),...);
+ *
+ * //delete multiple records
+ * $finder->deleteByPk(array( array($key1,$key2), array($key3,$key4), .. ));
+ * </code>
+ *
+ * @param mixed primary key values.
+ * @return int number of records deleted.
+ */
+ public function deleteByPk($keys)
+ {
+ if(func_num_args() > 1)
+ $keys = func_get_args();
+ return $this->getRecordGateway()->deleteRecordsByPk($this,(array)$keys);
+ }
+
+ /**
+ * Alias for deleteByPk()
+ */
+ public function deleteAllByPks($keys)
+ {
+ if(func_num_args() > 1)
+ $keys = func_get_args();
+ return $this->deleteByPk($keys);
+ }
+ /**
+ * Delete multiple records using a criteria.
+ * @param string|TActiveRecordCriteria SQL condition or criteria object.
+ * @param mixed parameter values.
+ * @return int number of records deleted.
+ */
+ public function deleteAll($criteria=null, $parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ $criteria = $this->getRecordCriteria($criteria,$parameters, $args);
+ return $this->getRecordGateway()->deleteRecordsByCriteria($this, $criteria);
+ }
+
+ /**
+ * Populates a new record with the query result.
+ * This is a wrapper of {@link createRecord}.
+ * @param array name value pair of record data
+ * @return TActiveRecord object record, null if data is empty.
+ */
+ protected function populateObject($data)
+ {
+ return self::createRecord(get_class($this), $data);
+ }
+
+ /**
+ * @param TDbDataReader data reader
+ * @return array the AR objects populated by the query result
+ * @since 3.1.2
+ */
+ protected function populateObjects($reader)
+ {
+ $result=array();
+ foreach($reader as $data)
+ $result[] = $this->populateObject($data);
+ return $result;
+ }
+
+ /**
+ * Create an AR instance specified by the AR class name and initial data.
+ * If the initial data is empty, the AR object will not be created and null will be returned.
+ * (You should use the "new" operator to create the AR instance in that case.)
+ * @param string the AR class name
+ * @param array initial data to be populated into the AR object.
+ * @return TActiveRecord the initialized AR object. Null if the initial data is empty.
+ * @since 3.1.2
+ */
+ public static function createRecord($type, $data)
+ {
+ if(empty($data))
+ return null;
+ $record=new $type($data);
+ $record->_recordState=self::STATE_LOADED;
+ return $record;
+ }
+
+ /**
+ * Find one single record that matches the criteria.
+ *
+ * Usage:
+ * <code>
+ * $finder->find('username = :name AND password = :pass',
+ * array(':name'=>$name, ':pass'=>$pass));
+ * $finder->find('username = ? AND password = ?', array($name, $pass));
+ * $finder->find('username = ? AND password = ?', $name, $pass);
+ * //$criteria is of TActiveRecordCriteria
+ * $finder->find($criteria); //the 2nd parameter for find() is ignored.
+ * </code>
+ *
+ * @param string|TActiveRecordCriteria SQL condition or criteria object.
+ * @param mixed parameter values.
+ * @return TActiveRecord matching record object. Null if no result is found.
+ */
+ public function find($criteria,$parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ $criteria = $this->getRecordCriteria($criteria,$parameters, $args);
+ $criteria->setLimit(1);
+ $data = $this->getRecordGateway()->findRecordsByCriteria($this,$criteria);
+ return $this->populateObject($data);
+ }
+
+ /**
+ * Same as find() but returns an array of objects.
+ *
+ * @param string|TActiveRecordCriteria SQL condition or criteria object.
+ * @param mixed parameter values.
+ * @return array matching record objects. Empty array if no result is found.
+ */
+ public function findAll($criteria=null,$parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ if($criteria!==null)
+ $criteria = $this->getRecordCriteria($criteria,$parameters, $args);
+ $result = $this->getRecordGateway()->findRecordsByCriteria($this,$criteria,true);
+ return $this->populateObjects($result);
+ }
+
+ /**
+ * Find one record using only the primary key or composite primary keys. Usage:
+ *
+ * <code>
+ * $finder->findByPk($primaryKey);
+ * $finder->findByPk($key1, $key2, ...);
+ * $finder->findByPk(array($key1,$key2,...));
+ * </code>
+ *
+ * @param mixed primary keys
+ * @return TActiveRecord. Null if no result is found.
+ */
+ public function findByPk($keys)
+ {
+ if($keys === null)
+ return null;
+ if(func_num_args() > 1)
+ $keys = func_get_args();
+ $data = $this->getRecordGateway()->findRecordByPK($this,$keys);
+ return $this->populateObject($data);
+ }
+
+ /**
+ * Find multiple records matching a list of primary or composite keys.
+ *
+ * For scalar primary keys:
+ * <code>
+ * $finder->findAllByPk($key1, $key2, ...);
+ * $finder->findAllByPk(array($key1, $key2, ...));
+ * </code>
+ *
+ * For composite keys:
+ * <code>
+ * $finder->findAllByPk(array($key1, $key2), array($key3, $key4), ...);
+ * $finder->findAllByPk(array(array($key1, $key2), array($key3, $key4), ...));
+ * </code>
+ * @param mixed primary keys
+ * @return array matching ActiveRecords. Empty array is returned if no result is found.
+ */
+ public function findAllByPks($keys)
+ {
+ if(func_num_args() > 1)
+ $keys = func_get_args();
+ $result = $this->getRecordGateway()->findRecordsByPks($this,(array)$keys);
+ return $this->populateObjects($result);
+ }
+
+ /**
+ * Find records using full SQL, returns corresponding record object.
+ * The names of the column retrieved must be defined in your Active Record
+ * class.
+ * @param string select SQL
+ * @param array $parameters
+ * @return TActiveRecord, null if no result is returned.
+ */
+ public function findBySql($sql,$parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ $criteria = $this->getRecordCriteria($sql,$parameters, $args);
+ $criteria->setLimit(1);
+ $data = $this->getRecordGateway()->findRecordBySql($this,$criteria);
+ return $this->populateObject($data);
+ }
+
+ /**
+ * Find records using full SQL, returns corresponding record object.
+ * The names of the column retrieved must be defined in your Active Record
+ * class.
+ * @param string select SQL
+ * @param array $parameters
+ * @return array matching active records. Empty array is returned if no result is found.
+ */
+ public function findAllBySql($sql,$parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ $criteria = $this->getRecordCriteria($sql,$parameters, $args);
+ $result = $this->getRecordGateway()->findRecordsBySql($this,$criteria);
+ return $this->populateObjects($result);
+ }
+
+ /**
+ * Fetches records using the sql clause "(fields) IN (values)", where
+ * fields is an array of column names and values is an array of values that
+ * the columns must have.
+ *
+ * This method is to be used by the relationship handler.
+ *
+ * @param TActiveRecordCriteria additional criteria
+ * @param array field names to match with "(fields) IN (values)" sql clause.
+ * @param array matching field values.
+ * @return array matching active records. Empty array is returned if no result is found.
+ */
+ public function findAllByIndex($criteria,$fields,$values)
+ {
+ $result = $this->getRecordGateway()->findRecordsByIndex($this,$criteria,$fields,$values);
+ return $this->populateObjects($result);
+ }
+
+ /**
+ * Find the number of records.
+ * @param string|TActiveRecordCriteria SQL condition or criteria object.
+ * @param mixed parameter values.
+ * @return int number of records.
+ */
+ public function count($criteria=null,$parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ if($criteria!==null)
+ $criteria = $this->getRecordCriteria($criteria,$parameters, $args);
+ return $this->getRecordGateway()->countRecords($this,$criteria);
+ }
+
+ /**
+ * Returns the active record relationship handler for $RELATION with key
+ * value equal to the $property value.
+ * @param string relationship/property name corresponding to keys in $RELATION array.
+ * @param array method call arguments.
+ * @return TActiveRecordRelation, null if the context or the handler doesn't exist
+ */
+ protected function getRelationHandler($name,$args=array())
+ {
+ if(($context=$this->createRelationContext($name)) !== null)
+ {
+ $criteria = $this->getRecordCriteria(count($args)>0 ? $args[0] : null, array_slice($args,1));
+ return $context->getRelationHandler($criteria);
+ }
+ else
+ return null;
+ }
+
+ /**
+ * Gets a static copy of the relationship context for given property (a key
+ * in $RELATIONS), returns null if invalid relationship. Keeps a null
+ * reference to all invalid relations called.
+ * @param string relationship/property name corresponding to keys in $RELATION array.
+ * @return TActiveRecordRelationContext object containing information on
+ * the active record relationships for given property, null if invalid relationship
+ * @since 3.1.2
+ */
+ protected function createRelationContext($name)
+ {
+ if(($definition=$this->getRecordRelation($name))!==null)
+ {
+ list($property, $relation) = $definition;
+ return new TActiveRecordRelationContext($this,$property,$relation);
+ }
+ else
+ return null;
+ }
+
+ /**
+ * Tries to load the relationship results for the given property. The $property
+ * value should correspond to an entry key in the $RELATION array.
+ * This method can be used to lazy load relationships.
+ * <code>
+ * class TeamRecord extends TActiveRecord
+ * {
+ * ...
+ *
+ * private $_players;
+ * public static $RELATION=array
+ * (
+ * 'players' => array(self::HAS_MANY, 'PlayerRecord'),
+ * );
+ *
+ * public function setPlayers($array)
+ * {
+ * $this->_players=$array;
+ * }
+ *
+ * public function getPlayers()
+ * {
+ * if($this->_players===null)
+ * $this->fetchResultsFor('players');
+ * return $this->_players;
+ * }
+ * }
+ * Usage example:
+ * $team = TeamRecord::finder()->findByPk(1);
+ * var_dump($team->players); //uses lazy load to fetch 'players' relation
+ * </code>
+ * @param string relationship/property name corresponding to keys in $RELATION array.
+ * @return boolean true if relationship exists, false otherwise.
+ * @since 3.1.2
+ */
+ protected function fetchResultsFor($property)
+ {
+ if( ($context=$this->createRelationContext($property)) !== null)
+ return $context->getRelationHandler()->fetchResultsInto($this);
+ else
+ return false;
+ }
+
+ /**
+ * Dynamic find method using parts of method name as search criteria.
+ * Method name starting with "findBy" only returns 1 record.
+ * Method name starting with "findAllBy" returns 0 or more records.
+ * Method name starting with "deleteBy" deletes records by the trail criteria.
+ * The condition is taken as part of the method name after "findBy", "findAllBy"
+ * or "deleteBy".
+ *
+ * The following are equivalent:
+ * <code>
+ * $finder->findByName($name)
+ * $finder->find('Name = ?', $name);
+ * </code>
+ * <code>
+ * $finder->findByUsernameAndPassword($name,$pass); // OR may be used
+ * $finder->findBy_Username_And_Password($name,$pass); // _OR_ may be used
+ * $finder->find('Username = ? AND Password = ?', $name, $pass);
+ * </code>
+ * <code>
+ * $finder->findAllByAge($age);
+ * $finder->findAll('Age = ?', $age);
+ * </code>
+ * <code>
+ * $finder->deleteAll('Name = ?', $name);
+ * $finder->deleteByName($name);
+ * </code>
+ * @return mixed single record if method name starts with "findBy", 0 or more records
+ * if method name starts with "findAllBy"
+ */
+ public function __call($method,$args)
+ {
+ $delete =false;
+ if(strncasecmp($method,'with',4)===0)
+ {
+ $property= $method[4]==='_' ? substr($method,5) : substr($method,4);
+ return $this->getRelationHandler($property, $args);
+ }
+ else if($findOne=strncasecmp($method,'findby',6)===0)
+ $condition = $method[6]==='_' ? substr($method,7) : substr($method,6);
+ else if(strncasecmp($method,'findallby',9)===0)
+ $condition = $method[9]==='_' ? substr($method,10) : substr($method,9);
+ else if($delete=strncasecmp($method,'deleteby',8)===0)
+ $condition = $method[8]==='_' ? substr($method,9) : substr($method,8);
+ else if($delete=strncasecmp($method,'deleteallby',11)===0)
+ $condition = $method[11]==='_' ? substr($method,12) : substr($method,11);
+ else
+ {
+ if($this->getInvalidFinderResult() == TActiveRecordInvalidFinderResult::Exception)
+ throw new TActiveRecordException('ar_invalid_finder_method',$method);
+ else
+ return null;
+ }
+
+ $criteria = $this->getRecordGateway()->getCommand($this)->createCriteriaFromString($method, $condition, $args);
+ if($delete)
+ return $this->deleteAll($criteria);
+ else
+ return $findOne ? $this->find($criteria) : $this->findAll($criteria);
+ }
+
+ /**
+ * @return TActiveRecordInvalidFinderResult Defaults to '{@link TActiveRecordInvalidFinderResult::Null Null}'.
+ * @see TActiveRecordManager::getInvalidFinderResult
+ * @since 3.1.5
+ */
+ public function getInvalidFinderResult()
+ {
+ if($this->_invalidFinderResult !== null)
+ return $this->_invalidFinderResult;
+
+ return self::getRecordManager()->getInvalidFinderResult();
+ }
+
+ /**
+ * Define the way an active record finder react if an invalid magic-finder invoked
+ *
+ * @param TActiveRecordInvalidFinderResult|null
+ * @see TActiveRecordManager::setInvalidFinderResult
+ * @since 3.1.5
+ */
+ public function setInvalidFinderResult($value)
+ {
+ if($value === null)
+ $this->_invalidFinderResult = null;
+ else
+ $this->_invalidFinderResult = TPropertyValue::ensureEnum($value, 'TActiveRecordInvalidFinderResult');
+ }
+
+ /**
+ * Create a new TSqlCriteria object from a string $criteria. The $args
+ * are additional parameters and are used in place of the $parameters
+ * if $parameters is not an array and $args is an arrary.
+ * @param string|TSqlCriteria sql criteria
+ * @param mixed parameters passed by the user.
+ * @param array additional parameters obtained from function_get_args().
+ * @return TSqlCriteria criteria object.
+ */
+ protected function getRecordCriteria($criteria, $parameters, $args=array())
+ {
+ if(is_string($criteria))
+ {
+ $useArgs = !is_array($parameters) && is_array($args);
+ return new TActiveRecordCriteria($criteria,$useArgs ? $args : $parameters);
+ }
+ else if($criteria instanceof TSqlCriteria)
+ return $criteria;
+ else
+ return new TActiveRecordCriteria();
+ //throw new TActiveRecordException('ar_invalid_criteria');
+ }
+
+ /**
+ * Raised when a command is prepared and parameter binding is completed.
+ * The parameter object is TDataGatewayEventParameter of which the
+ * {@link TDataGatewayEventParameter::getCommand Command} property can be
+ * inspected to obtain the sql query to be executed.
+ *
+ * Note well that the finder objects obtained from ActiveRecord::finder()
+ * method are static objects. This means that the event handlers are
+ * bound to a static finder object and not to each distinct active record object.
+ * @param TDataGatewayEventParameter
+ */
+ public function onCreateCommand($param)
+ {
+ $this->raiseEvent('OnCreateCommand', $this, $param);
+ }
+
+ /**
+ * Raised when a command is executed and the result from the database was returned.
+ * The parameter object is TDataGatewayResultEventParameter of which the
+ * {@link TDataGatewayEventParameter::getResult Result} property contains
+ * the data return from the database. The data returned can be changed
+ * by setting the {@link TDataGatewayEventParameter::setResult Result} property.
+ *
+ * Note well that the finder objects obtained from ActiveRecord::finder()
+ * method are static objects. This means that the event handlers are
+ * bound to a static finder object and not to each distinct active record object.
+ * @param TDataGatewayResultEventParameter
+ */
+ public function onExecuteCommand($param)
+ {
+ $this->raiseEvent('OnExecuteCommand', $this, $param);
+ }
+
+ /**
+ * Raised before the record attempt to insert its data into the database.
+ * To prevent the insert operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
+ * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
+ */
+ public function onInsert($param)
+ {
+ $this->raiseEvent('OnInsert', $this, $param);
+ }
+
+ /**
+ * Raised before the record attempt to delete its data from the database.
+ * To prevent the delete operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
+ * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
+ */
+ public function onDelete($param)
+ {
+ $this->raiseEvent('OnDelete', $this, $param);
+ }
+
+ /**
+ * Raised before the record attempt to update its data in the database.
+ * To prevent the update operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
+ * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
+ */
+ public function onUpdate($param)
+ {
+ $this->raiseEvent('OnUpdate', $this, $param);
+ }
+
+ /**
+ * Retrieves the column value according to column name.
+ * This method is used internally.
+ * @param string the column name (as defined in database schema)
+ * @return mixed the corresponding column value
+ * @since 3.1.1
+ */
+ public function getColumnValue($columnName)
+ {
+ $className=get_class($this);
+ if(isset(self::$_columnMapping[$className][$columnName]))
+ $columnName=self::$_columnMapping[$className][$columnName];
+ return $this->$columnName;
+ }
+
+ /**
+ * Sets the column value according to column name.
+ * This method is used internally.
+ * @param string the column name (as defined in database schema)
+ * @param mixed the corresponding column value
+ * @since 3.1.1
+ */
+ public function setColumnValue($columnName,$value)
+ {
+ $className=get_class($this);
+ if(isset(self::$_columnMapping[$className][$columnName]))
+ $columnName=self::$_columnMapping[$className][$columnName];
+ $this->$columnName=$value;
+ }
+
+ /**
+ * @param string relation property name
+ * @return array relation definition for the specified property
+ * @since 3.1.2
+ */
+ public function getRecordRelation($property)
+ {
+ $className=get_class($this);
+ $property=strtolower($property);
+ return isset(self::$_relations[$className][$property])?self::$_relations[$className][$property]:null;
+ }
+
+ /**
+ * @return array all relation definitions declared in the AR class
+ * @since 3.1.2
+ */
+ public function getRecordRelations()
+ {
+ return self::$_relations[get_class($this)];
+ }
+
+ /**
+ * @param string AR property name
+ * @return boolean whether a relation is declared for the specified AR property
+ * @since 3.1.2
+ */
+ public function hasRecordRelation($property)
+ {
+ return isset(self::$_relations[get_class($this)][strtolower($property)]);
+ }
+
+ /**
+ * Return record data as array
+ * @return array of column name and column values
+ * @since 3.2.4
+ */
+ public function toArray(){
+ $result=array();
+ foreach($this->getRecordTableInfo()->getLowerCaseColumnNames() as $columnName){
+ $result[$columnName]=$this->getColumnValue($columnName);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return record data as JSON
+ * @return JSON
+ * @since 3.2.4
+ */
+ public function toJSON(){
+ return json_encode($this->toArray());
+ }
+}
+
+/**
+ * TActiveRecordChangeEventParameter class
+ *
+ * TActiveRecordChangeEventParameter encapsulates the parameter data for
+ * ActiveRecord change commit events that are broadcasted. The following change events
+ * may be raise: {@link TActiveRecord::OnInsert}, {@link TActiveRecord::OnUpdate} and
+ * {@link TActiveRecord::OnDelete}. The {@link setIsValid IsValid} parameter can
+ * be set to false to prevent the requested change event to be performed.
+ *
+ * @author Wei Zhuo<weizhuo@gmail.com>
+ * @package System.Data.ActiveRecord
+ * @since 3.1.2
+ */
+class TActiveRecordChangeEventParameter extends TEventParameter
+{
+ private $_isValid=true;
+
+ /**
+ * @return boolean whether the event should be performed.
+ */
+ public function getIsValid()
+ {
+ return $this->_isValid;
+ }
+
+ /**
+ * @param boolean set to false to prevent the event.
+ */
+ public function setIsValid($value)
+ {
+ $this->_isValid = TPropertyValue::ensureBoolean($value);
+ }
+}
+
+/**
+ * TActiveRecordInvalidFinderResult class.
+ * TActiveRecordInvalidFinderResult defines the enumerable type for possible results
+ * if an invalid {@link TActiveRecord::__call magic-finder} invoked.
+ *
+ * The following enumerable values are defined:
+ * - Null: return null (default)
+ * - Exception: throws a TActiveRecordException
+ *
+ * @author Yves Berkholz <godzilla80@gmx.net>
+ * @package System.Data.ActiveRecord
+ * @see TActiveRecordManager::setInvalidFinderResult
+ * @see TActiveRecordConfig::setInvalidFinderResult
+ * @see TActiveRecord::setInvalidFinderResult
+ * @since 3.1.5
+ */
+class TActiveRecordInvalidFinderResult extends TEnumerable
+{
+ const Null = 'Null';
+ const Exception = 'Exception';
+}
diff --git a/lib/prado/framework/Data/ActiveRecord/TActiveRecordConfig.php b/lib/prado/framework/Data/ActiveRecord/TActiveRecordConfig.php
new file mode 100644
index 0000000..f2c7e0b
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/TActiveRecordConfig.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * TActiveRecordConfig class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord
+ */
+
+Prado::using('System.Data.TDataSourceConfig');
+Prado::using('System.Data.ActiveRecord.TActiveRecordManager');
+
+/**
+ * TActiveRecordConfig module configuration class.
+ *
+ * Database configuration for the default ActiveRecord manager instance.
+ *
+ * Example: application.xml configuration
+ * <code>
+ * <modules>
+ * <module class="System.Data.ActiveRecord.TActiveRecordConfig" EnableCache="true">
+ * <database ConnectionString="mysql:host=localhost;dbname=test"
+ * Username="dbuser" Password="dbpass" />
+ * </module>
+ * </modules>
+ * </code>
+ *
+ * MySQL database definition:
+ * <code>
+ * CREATE TABLE `blogs` (
+ * `blog_id` int(10) unsigned NOT NULL auto_increment,
+ * `blog_name` varchar(255) NOT NULL,
+ * `blog_author` varchar(255) NOT NULL,
+ * PRIMARY KEY (`blog_id`)
+ * ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+ * </code>
+ *
+ * Record php class:
+ * <code>
+ * class Blogs extends TActiveRecord
+ * {
+ * public $blog_id;
+ * public $blog_name;
+ * public $blog_author;
+ *
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * </code>
+ *
+ * Usage example:
+ * <code>
+ * class Home extends TPage
+ * {
+ * function onLoad($param)
+ * {
+ * $blogs = Blogs::finder()->findAll();
+ * print_r($blogs);
+ * }
+ * }
+ * </code>
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord
+ * @since 3.1
+ */
+class TActiveRecordConfig extends TDataSourceConfig
+{
+ const DEFAULT_MANAGER_CLASS = 'System.Data.ActiveRecord.TActiveRecordManager';
+ const DEFAULT_GATEWAY_CLASS = 'System.Data.ActiveRecord.TActiveRecordGateway';
+
+ /**
+ * Defaults to {@link TActiveRecordConfig::DEFAULT_GATEWAY_CLASS DEFAULT_MANAGER_CLASS}
+ * @var string
+ */
+ private $_managerClass = self::DEFAULT_MANAGER_CLASS;
+
+ /**
+ * Defaults to {@link TActiveRecordConfig::DEFAULT_GATEWAY_CLASS DEFAULT_GATEWAY_CLASS}
+ * @var string
+ */
+ private $_gatewayClass = self::DEFAULT_GATEWAY_CLASS;
+
+ /**
+ * @var TActiveRecordManager
+ */
+ private $_manager = null;
+
+ private $_enableCache=false;
+
+ /**
+ * Defaults to '{@link TActiveRecordInvalidFinderResult::Null Null}'
+ *
+ * @var TActiveRecordInvalidFinderResult
+ * @since 3.1.5
+ */
+ private $_invalidFinderResult = TActiveRecordInvalidFinderResult::Null;
+
+ /**
+ * Initialize the active record manager.
+ * @param TXmlDocument xml configuration.
+ */
+ public function init($xml)
+ {
+ parent::init($xml);
+ $manager = $this -> getManager();
+ if($this->getEnableCache())
+ $manager->setCache($this->getApplication()->getCache());
+ $manager->setDbConnection($this->getDbConnection());
+ $manager->setInvalidFinderResult($this->getInvalidFinderResult());
+ $manager->setGatewayClass($this->getGatewayClass());
+ }
+
+ /**
+ * @return TActiveRecordManager
+ */
+ public function getManager() {
+ if($this->_manager === null)
+ $this->_manager = Prado::createComponent($this -> getManagerClass());
+ return TActiveRecordManager::getInstance($this->_manager);
+ }
+
+ /**
+ * Set implementation class of ActiveRecordManager
+ * @param string $value
+ */
+ public function setManagerClass($value)
+ {
+ $this->_managerClass = TPropertyValue::ensureString($value);
+ }
+
+ /**
+ * @return string the implementation class of ActiveRecordManager. Defaults to {@link TActiveRecordConfig::DEFAULT_GATEWAY_CLASS DEFAULT_MANAGER_CLASS}
+ */
+ public function getManagerClass()
+ {
+ return $this->_managerClass;
+ }
+
+ /**
+ * Set implementation class of ActiveRecordGateway
+ * @param string $value
+ */
+ public function setGatewayClass($value)
+ {
+ $this->_gatewayClass = TPropertyValue::ensureString($value);
+ }
+
+ /**
+ * @return string the implementation class of ActiveRecordGateway. Defaults to {@link TActiveRecordConfig::DEFAULT_GATEWAY_CLASS DEFAULT_GATEWAY_CLASS}
+ */
+ public function getGatewayClass()
+ {
+ return $this->_gatewayClass;
+ }
+
+ /**
+ * Set true to cache the table meta data.
+ * @param boolean true to cache sqlmap instance.
+ */
+ public function setEnableCache($value)
+ {
+ $this->_enableCache = TPropertyValue::ensureBoolean($value);
+ }
+
+ /**
+ * @return boolean true if table meta data should be cached, false otherwise.
+ */
+ public function getEnableCache()
+ {
+ return $this->_enableCache;
+ }
+
+ /**
+ * @return TActiveRecordInvalidFinderResult Defaults to '{@link TActiveRecordInvalidFinderResult::Null Null}'.
+ * @see setInvalidFinderResult
+ * @since 3.1.5
+ */
+ public function getInvalidFinderResult()
+ {
+ return $this->_invalidFinderResult;
+ }
+
+ /**
+ * Define the way an active record finder react if an invalid magic-finder invoked
+ *
+ * @param TActiveRecordInvalidFinderResult
+ * @see getInvalidFinderResult
+ * @since 3.1.5
+ */
+ public function setInvalidFinderResult($value)
+ {
+ $this->_invalidFinderResult = TPropertyValue::ensureEnum($value, 'TActiveRecordInvalidFinderResult');
+ }
+}
diff --git a/lib/prado/framework/Data/ActiveRecord/TActiveRecordCriteria.php b/lib/prado/framework/Data/ActiveRecord/TActiveRecordCriteria.php
new file mode 100644
index 0000000..612658e
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/TActiveRecordCriteria.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * TActiveRecordCriteria class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord
+ */
+
+Prado::using('System.Data.DataGateway.TSqlCriteria');
+
+/**
+ * Search criteria for Active Record.
+ *
+ * Criteria object for active record finder methods. Usage:
+ * <code>
+ * $criteria = new TActiveRecordCriteria;
+ * $criteria->Condition = 'username = :name AND password = :pass';
+ * $criteria->Parameters[':name'] = 'admin';
+ * $criteria->Parameters[':pass'] = 'prado';
+ * $criteria->OrdersBy['level'] = 'desc';
+ * $criteria->OrdersBy['name'] = 'asc';
+ * $criteria->Limit = 10;
+ * $criteria->Offset = 20;
+ * </code>
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord
+ * @since 3.1
+ */
+class TActiveRecordCriteria extends TSqlCriteria
+{
+
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/TActiveRecordGateway.php b/lib/prado/framework/Data/ActiveRecord/TActiveRecordGateway.php
new file mode 100644
index 0000000..e631a73
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/TActiveRecordGateway.php
@@ -0,0 +1,423 @@
+<?php
+/**
+ * TActiveRecordGateway, TActiveRecordStatementType, TActiveRecordEventParameter classes file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord
+ */
+
+/**
+ * TActiveRecordGateway excutes the SQL command queries and returns the data
+ * record as arrays (for most finder methods).
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord
+ * @since 3.1
+ */
+class TActiveRecordGateway extends TComponent
+{
+ private $_manager;
+ private $_tables=array(); //table cache
+ private $_meta=array(); //meta data cache.
+ private $_commandBuilders=array();
+ private $_currentRecord;
+
+ /**
+ * Constant name for specifying optional table name in TActiveRecord.
+ */
+ const TABLE_CONST='TABLE';
+ /**
+ * Method name for returning optional table name in in TActiveRecord
+ */
+ const TABLE_METHOD='table';
+
+ /**
+ * Record gateway constructor.
+ * @param TActiveRecordManager $manager
+ */
+ public function __construct(TActiveRecordManager $manager)
+ {
+ $this->_manager=$manager;
+ }
+
+ /**
+ * @return TActiveRecordManager record manager.
+ */
+ protected function getManager()
+ {
+ return $this->_manager;
+ }
+
+ /**
+ * Gets the table name from the 'TABLE' constant of the active record
+ * class if defined, otherwise use the class name as table name.
+ * @param TActiveRecord active record instance
+ * @return string table name for the given record class.
+ */
+ protected function getRecordTableName(TActiveRecord $record)
+ {
+ $class = new ReflectionClass($record);
+ if($class->hasConstant(self::TABLE_CONST))
+ {
+ $value = $class->getConstant(self::TABLE_CONST);
+ if(empty($value))
+ throw new TActiveRecordException('ar_invalid_tablename_property',
+ get_class($record),self::TABLE_CONST);
+ return $value;
+ }
+ elseif ($class->hasMethod(self::TABLE_METHOD))
+ {
+ $value = $record->{self::TABLE_METHOD}();
+ if(empty($value))
+ throw new TActiveRecordException('ar_invalid_tablename_method',
+ get_class($record),self::TABLE_METHOD);
+ return $value;
+ }
+ else
+ return strtolower(get_class($record));
+ }
+
+ /**
+ * Returns table information, trys the application cache first.
+ * @param TActiveRecord $record
+ * @return TDbTableInfo table information.
+ */
+ public function getRecordTableInfo(TActiveRecord $record)
+ {
+ $tableName = $this->getRecordTableName($record);
+ return $this->getTableInfo($record->getDbConnection(), $tableName);
+ }
+
+ /**
+ * Returns table information for table in the database connection.
+ * @param TDbConnection database connection
+ * @param string table name
+ * @return TDbTableInfo table details.
+ */
+ public function getTableInfo(TDbConnection $connection, $tableName)
+ {
+ $connStr = $connection->getConnectionString();
+ $key = $connStr.$tableName;
+ if(!isset($this->_tables[$key]))
+ {
+ //call this first to ensure that unserializing the cache
+ //will find the correct driver dependent classes.
+ if(!isset($this->_meta[$connStr]))
+ {
+ Prado::using('System.Data.Common.TDbMetaData');
+ $this->_meta[$connStr] = TDbMetaData::getInstance($connection);
+ }
+
+ $tableInfo = null;
+ if(($cache=$this->getManager()->getCache())!==null)
+ $tableInfo = $cache->get($key);
+ if(empty($tableInfo))
+ {
+ $tableInfo = $this->_meta[$connStr]->getTableInfo($tableName);
+ if($cache!==null)
+ $cache->set($key, $tableInfo);
+ }
+ $this->_tables[$key] = $tableInfo;
+ }
+ return $this->_tables[$key];
+ }
+
+ /**
+ * @param TActiveRecord $record
+ * @return TDataGatewayCommand
+ */
+ public function getCommand(TActiveRecord $record)
+ {
+ $conn = $record->getDbConnection();
+ $connStr = $conn->getConnectionString();
+ $tableInfo = $this->getRecordTableInfo($record);
+ if(!isset($this->_commandBuilders[$connStr]))
+ {
+ $builder = $tableInfo->createCommandBuilder($record->getDbConnection());
+ Prado::using('System.Data.DataGateway.TDataGatewayCommand');
+ $command = new TDataGatewayCommand($builder);
+ $command->OnCreateCommand[] = array($this, 'onCreateCommand');
+ $command->OnExecuteCommand[] = array($this, 'onExecuteCommand');
+ $this->_commandBuilders[$connStr] = $command;
+
+ }
+ $this->_commandBuilders[$connStr]->getBuilder()->setTableInfo($tableInfo);
+ $this->_currentRecord=$record;
+ return $this->_commandBuilders[$connStr];
+ }
+
+ /**
+ * Raised when a command is prepared and parameter binding is completed.
+ * The parameter object is TDataGatewayEventParameter of which the
+ * {@link TDataGatewayEventParameter::getCommand Command} property can be
+ * inspected to obtain the sql query to be executed.
+ * This method also raises the OnCreateCommand event on the ActiveRecord
+ * object calling this gateway.
+ * @param TDataGatewayCommand originator $sender
+ * @param TDataGatewayEventParameter
+ */
+ public function onCreateCommand($sender, $param)
+ {
+ $this->raiseEvent('OnCreateCommand', $this, $param);
+ if($this->_currentRecord!==null)
+ $this->_currentRecord->onCreateCommand($param);
+ }
+
+ /**
+ * Raised when a command is executed and the result from the database was returned.
+ * The parameter object is TDataGatewayResultEventParameter of which the
+ * {@link TDataGatewayEventParameter::getResult Result} property contains
+ * the data return from the database. The data returned can be changed
+ * by setting the {@link TDataGatewayEventParameter::setResult Result} property.
+ * This method also raises the OnCreateCommand event on the ActiveRecord
+ * object calling this gateway.
+ * @param TDataGatewayCommand originator $sender
+ * @param TDataGatewayResultEventParameter
+ */
+ public function onExecuteCommand($sender, $param)
+ {
+ $this->raiseEvent('OnExecuteCommand', $this, $param);
+ if($this->_currentRecord!==null)
+ $this->_currentRecord->onExecuteCommand($param);
+ }
+
+ /**
+ * Returns record data matching the given primary key(s). If the table uses
+ * composite key, specify the name value pairs as an array.
+ * @param TActiveRecord active record instance.
+ * @param array primary name value pairs
+ * @return array record data
+ */
+ public function findRecordByPK(TActiveRecord $record,$keys)
+ {
+ $command = $this->getCommand($record);
+ return $command->findByPk($keys);
+ }
+
+ /**
+ * Returns records matching the list of given primary keys.
+ * @param TActiveRecord active record instance.
+ * @param array list of primary name value pairs
+ * @return array matching data.
+ */
+ public function findRecordsByPks(TActiveRecord $record, $keys)
+ {
+ return $this->getCommand($record)->findAllByPk($keys);
+ }
+
+
+ /**
+ * Returns record data matching the given critera. If $iterator is true, it will
+ * return multiple rows as TDbDataReader otherwise it returns the <b>first</b> row data.
+ * @param TActiveRecord active record finder instance.
+ * @param TActiveRecordCriteria search criteria.
+ * @param boolean true to return multiple rows as iterator, false returns first row.
+ * @return mixed matching data.
+ */
+ public function findRecordsByCriteria(TActiveRecord $record, $criteria, $iterator=false)
+ {
+ $command = $this->getCommand($record);
+ return $iterator ? $command->findAll($criteria) : $command->find($criteria);
+ }
+
+ /**
+ * Return record data from sql query.
+ * @param TActiveRecord active record finder instance.
+ * @param TActiveRecordCriteria sql query
+ * @return array result.
+ */
+ public function findRecordBySql(TActiveRecord $record, $criteria)
+ {
+ return $this->getCommand($record)->findBySql($criteria);
+ }
+
+ /**
+ * Return record data from sql query.
+ * @param TActiveRecord active record finder instance.
+ * @param TActiveRecordCriteria sql query
+ * @return TDbDataReader result iterator.
+ */
+ public function findRecordsBySql(TActiveRecord $record, $criteria)
+ {
+ return $this->getCommand($record)->findAllBySql($criteria);
+ }
+
+ public function findRecordsByIndex(TActiveRecord $record, $criteria, $fields, $values)
+ {
+ return $this->getCommand($record)->findAllByIndex($criteria,$fields,$values);
+ }
+
+ /**
+ * Returns the number of records that match the given criteria.
+ * @param TActiveRecord active record finder instance.
+ * @param TActiveRecordCriteria search criteria
+ * @return int number of records.
+ */
+ public function countRecords(TActiveRecord $record, $criteria)
+ {
+ return $this->getCommand($record)->count($criteria);
+ }
+
+ /**
+ * Insert a new record.
+ * @param TActiveRecord new record.
+ * @return int number of rows affected.
+ */
+ public function insert(TActiveRecord $record)
+ {
+ //$this->updateAssociatedRecords($record,true);
+ $result = $this->getCommand($record)->insert($this->getInsertValues($record));
+ if($result)
+ $this->updatePostInsert($record);
+ //$this->updateAssociatedRecords($record);
+ return $result;
+ }
+
+ /**
+ * Sets the last insert ID to the corresponding property of the record if available.
+ * @param TActiveRecord record for insertion
+ */
+ protected function updatePostInsert($record)
+ {
+ $command = $this->getCommand($record);
+ $tableInfo = $command->getTableInfo();
+ foreach($tableInfo->getColumns() as $name => $column)
+ {
+ if($column->hasSequence())
+ $record->setColumnValue($name,$command->getLastInsertID($column->getSequenceName()));
+ }
+ }
+
+ /**
+ * @param TActiveRecord record
+ * @return array insert values.
+ */
+ protected function getInsertValues(TActiveRecord $record)
+ {
+ $values=array();
+ $tableInfo = $this->getCommand($record)->getTableInfo();
+ foreach($tableInfo->getColumns() as $name=>$column)
+ {
+ if($column->getIsExcluded())
+ continue;
+ $value = $record->getColumnValue($name);
+ if(!$column->getAllowNull() && $value===null && !$column->hasSequence() && ($column->getDefaultValue() === TDbTableColumn::UNDEFINED_VALUE))
+ {
+ throw new TActiveRecordException(
+ 'ar_value_must_not_be_null', get_class($record),
+ $tableInfo->getTableFullName(), $name);
+ }
+ if($value!==null)
+ $values[$name] = $value;
+ }
+ return $values;
+ }
+
+ /**
+ * Update the record.
+ * @param TActiveRecord dirty record.
+ * @return int number of rows affected.
+ */
+ public function update(TActiveRecord $record)
+ {
+ //$this->updateAssociatedRecords($record,true);
+ list($data, $keys) = $this->getUpdateValues($record);
+ $result = $this->getCommand($record)->updateByPk($data, $keys);
+ //$this->updateAssociatedRecords($record);
+ return $result;
+ }
+
+ protected function getUpdateValues(TActiveRecord $record)
+ {
+ $values=array();
+ $tableInfo = $this->getCommand($record)->getTableInfo();
+ $primary=array();
+ foreach($tableInfo->getColumns() as $name=>$column)
+ {
+ if($column->getIsExcluded())
+ continue;
+ $value = $record->getColumnValue($name);
+ if(!$column->getAllowNull() && $value===null && ($column->getDefaultValue() === TDbTableColumn::UNDEFINED_VALUE))
+ {
+ throw new TActiveRecordException(
+ 'ar_value_must_not_be_null', get_class($record),
+ $tableInfo->getTableFullName(), $name);
+ }
+ if($column->getIsPrimaryKey())
+ $primary[$name] = $value;
+ else
+ $values[$name] = $value;
+ }
+ return array($values,$primary);
+ }
+
+ protected function updateAssociatedRecords(TActiveRecord $record,$updateBelongsTo=false)
+ {
+ $context = new TActiveRecordRelationContext($record);
+ return $context->updateAssociatedRecords($updateBelongsTo);
+ }
+
+ /**
+ * Delete the record.
+ * @param TActiveRecord record to be deleted.
+ * @return int number of rows affected.
+ */
+ public function delete(TActiveRecord $record)
+ {
+ return $this->getCommand($record)->deleteByPk($this->getPrimaryKeyValues($record));
+ }
+
+ protected function getPrimaryKeyValues(TActiveRecord $record)
+ {
+ $tableInfo = $this->getCommand($record)->getTableInfo();
+ $primary=array();
+ foreach($tableInfo->getColumns() as $name=>$column)
+ {
+ if($column->getIsPrimaryKey())
+ $primary[$name] = $record->getColumnValue($name);
+ }
+ return $primary;
+ }
+
+ /**
+ * Delete multiple records using primary keys.
+ * @param TActiveRecord finder instance.
+ * @return int number of rows deleted.
+ */
+ public function deleteRecordsByPk(TActiveRecord $record, $keys)
+ {
+ return $this->getCommand($record)->deleteByPk($keys);
+ }
+
+ /**
+ * Delete multiple records by criteria.
+ * @param TActiveRecord active record finder instance.
+ * @param TActiveRecordCriteria search criteria
+ * @return int number of records.
+ */
+ public function deleteRecordsByCriteria(TActiveRecord $record, $criteria)
+ {
+ return $this->getCommand($record)->delete($criteria);
+ }
+
+ /**
+ * Raise the corresponding command event, insert, update, delete or select.
+ * @param string command type
+ * @param TDbCommand sql command to be executed.
+ * @param TActiveRecord active record
+ * @param TActiveRecordCriteria data for the command.
+ */
+ protected function raiseCommandEvent($event,$command,$record,$criteria)
+ {
+ if(!($criteria instanceof TSqlCriteria))
+ $criteria = new TActiveRecordCriteria(null,$criteria);
+ $param = new TActiveRecordEventParameter($command,$record,$criteria);
+ $manager = $record->getRecordManager();
+ $manager->{$event}($param);
+ $record->{$event}($param);
+ }
+}
+
diff --git a/lib/prado/framework/Data/ActiveRecord/TActiveRecordManager.php b/lib/prado/framework/Data/ActiveRecord/TActiveRecordManager.php
new file mode 100644
index 0000000..1e836d5
--- /dev/null
+++ b/lib/prado/framework/Data/ActiveRecord/TActiveRecordManager.php
@@ -0,0 +1,162 @@
+<?php
+/**
+ * TActiveRecordManager class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link https://github.com/pradosoft/prado
+ * @copyright Copyright &copy; 2005-2015 The PRADO Group
+ * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
+ * @package System.Data.ActiveRecord
+ */
+
+Prado::using('System.Data.TDbConnection');
+Prado::using('System.Data.ActiveRecord.TActiveRecord');
+Prado::using('System.Data.ActiveRecord.Exceptions.TActiveRecordException');
+Prado::using('System.Data.ActiveRecord.TActiveRecordGateway');
+
+/**
+ * TActiveRecordManager provides the default DB connection,
+ * default active record gateway, and table meta data inspector.
+ *
+ * The default connection can be set as follows:
+ * <code>
+ * TActiveRecordManager::getInstance()->setDbConnection($conn);
+ * </code>
+ * All new active record created after setting the
+ * {@link DbConnection setDbConnection()} will use that connection unless
+ * the custom ActiveRecord class overrides the ActiveRecord::getDbConnection().
+ *
+ * Set the {@link setCache Cache} property to an ICache object to allow
+ * the active record gateway to cache the table meta data information.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.ActiveRecord
+ * @since 3.1
+ */
+class TActiveRecordManager extends TComponent
+{
+ const DEFAULT_GATEWAY_CLASS = 'System.Data.ActiveRecord.TActiveRecordGateway';
+
+ /**
+ * Defaults to {@link TActiveRecordManager::DEFAULT_GATEWAY_CLASS DEFAULT_GATEWAY_CLASS}
+ * @var string
+ */
+ private $_gatewayClass = self::DEFAULT_GATEWAY_CLASS;
+
+ private $_gateway;
+ private $_meta=array();
+ private $_connection;
+
+ private $_cache;
+
+ /**
+ * Defaults to '{@link TActiveRecordInvalidFinderResult::Null Null}'
+ *
+ * @var TActiveRecordInvalidFinderResult
+ * @since 3.1.5
+ */
+ private $_invalidFinderResult = TActiveRecordInvalidFinderResult::Null;
+
+ /**
+ * @return ICache application cache.
+ */
+ public function getCache()
+ {
+ return $this->_cache;
+ }
+
+ /**
+ * @param ICache application cache
+ */
+ public function setCache($value)
+ {
+ $this->_cache=$value;
+ }
+
+ /**
+ * @param TDbConnection default database connection
+ */
+ public function setDbConnection($conn)
+ {
+ $this->_connection=$conn;
+ }
+
+ /**
+ * @return TDbConnection default database connection
+ */
+ public function getDbConnection()
+ {
+ return $this->_connection;
+ }
+
+ /**
+ * @return TActiveRecordManager static instance of record manager.
+ */
+ public static function getInstance($self=null)
+ {
+ static $instance;
+ if($self!==null)
+ $instance=$self;
+ else if($instance===null)
+ $instance = new self;
+ return $instance;
+ }
+
+ /**
+ * @return TActiveRecordGateway record gateway.
+ */
+ public function getRecordGateway()
+ {
+ if($this->_gateway === null) {
+ $this->_gateway = $this->createRecordGateway();
+ }
+ return $this->_gateway;
+ }
+
+ /**
+ * @return TActiveRecordGateway default record gateway.
+ */
+ protected function createRecordGateway()
+ {
+ return Prado::createComponent($this->getGatewayClass(), $this);
+ }
+
+ /**
+ * Set implementation class of ActiveRecordGateway
+ * @param string $value
+ */
+ public function setGatewayClass($value)
+ {
+ $this->_gateway = null;
+ $this->_gatewayClass = (string)$value;
+ }
+
+ /**
+ * @return string the implementation class of ActiveRecordGateway. Defaults to {@link TActiveRecordManager::DEFAULT_GATEWAY_CLASS DEFAULT_GATEWAY_CLASS}
+ */
+ public function getGatewayClass()
+ {
+ return $this->_gatewayClass;
+ }
+
+ /**
+ * @return TActiveRecordInvalidFinderResult Defaults to '{@link TActiveRecordInvalidFinderResult::Null Null}'.
+ * @since 3.1.5
+ * @see setInvalidFinderResult
+ */
+ public function getInvalidFinderResult()
+ {
+ return $this->_invalidFinderResult;
+ }
+
+ /**
+ * Define the way an active record finder react if an invalid magic-finder invoked
+ * @param TActiveRecordInvalidFinderResult
+ * @since 3.1.5
+ * @see getInvalidFinderResult
+ */
+ public function setInvalidFinderResult($value)
+ {
+ $this->_invalidFinderResult = TPropertyValue::ensureEnum($value, 'TActiveRecordInvalidFinderResult');
+ }
+}