summaryrefslogtreecommitdiff
path: root/lib/prado/framework/Data
diff options
context:
space:
mode:
Diffstat (limited to 'lib/prado/framework/Data')
-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
-rw-r--r--lib/prado/framework/Data/Common/Mssql/TMssqlCommandBuilder.php171
-rw-r--r--lib/prado/framework/Data/Common/Mssql/TMssqlMetaData.php290
-rw-r--r--lib/prado/framework/Data/Common/Mssql/TMssqlTableColumn.php62
-rw-r--r--lib/prado/framework/Data/Common/Mssql/TMssqlTableInfo.php62
-rw-r--r--lib/prado/framework/Data/Common/Mysql/TMysqlCommandBuilder.php24
-rw-r--r--lib/prado/framework/Data/Common/Mysql/TMysqlMetaData.php405
-rw-r--r--lib/prado/framework/Data/Common/Mysql/TMysqlTableColumn.php70
-rw-r--r--lib/prado/framework/Data/Common/Mysql/TMysqlTableInfo.php56
-rw-r--r--lib/prado/framework/Data/Common/Oracle/TOracleCommandBuilder.php153
-rw-r--r--lib/prado/framework/Data/Common/Oracle/TOracleMetaData.php374
-rw-r--r--lib/prado/framework/Data/Common/Oracle/TOracleTableColumn.php48
-rw-r--r--lib/prado/framework/Data/Common/Oracle/TOracleTableInfo.php156
-rw-r--r--lib/prado/framework/Data/Common/Pgsql/TPgsqlCommandBuilder.php67
-rw-r--r--lib/prado/framework/Data/Common/Pgsql/TPgsqlMetaData.php448
-rw-r--r--lib/prado/framework/Data/Common/Pgsql/TPgsqlTableColumn.php47
-rw-r--r--lib/prado/framework/Data/Common/Pgsql/TPgsqlTableInfo.php56
-rw-r--r--lib/prado/framework/Data/Common/Sqlite/TSqliteCommandBuilder.php45
-rw-r--r--lib/prado/framework/Data/Common/Sqlite/TSqliteMetaData.php202
-rw-r--r--lib/prado/framework/Data/Common/Sqlite/TSqliteTableColumn.php62
-rw-r--r--lib/prado/framework/Data/Common/Sqlite/TSqliteTableInfo.php45
-rw-r--r--lib/prado/framework/Data/Common/TDbCommandBuilder.php505
-rw-r--r--lib/prado/framework/Data/Common/TDbMetaData.php192
-rw-r--r--lib/prado/framework/Data/Common/TDbTableColumn.php197
-rw-r--r--lib/prado/framework/Data/Common/TDbTableInfo.php164
-rw-r--r--lib/prado/framework/Data/DataGateway/TDataGatewayCommand.php544
-rw-r--r--lib/prado/framework/Data/DataGateway/TSqlCriteria.php283
-rw-r--r--lib/prado/framework/Data/DataGateway/TTableGateway.php476
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TDiscriminator.php229
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TInlineParameterMapParser.php77
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TParameterMap.php208
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TParameterProperty.php148
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TResultMap.php198
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TResultProperty.php342
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TSimpleDynamicParser.php43
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TSqlMapCacheModel.php242
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TSqlMapStatement.php444
-rw-r--r--lib/prado/framework/Data/SqlMap/Configuration/TSqlMapXmlConfiguration.php801
-rw-r--r--lib/prado/framework/Data/SqlMap/DataMapper/TFastSqlMapApplicationCache.php87
-rw-r--r--lib/prado/framework/Data/SqlMap/DataMapper/TLazyLoadList.php141
-rw-r--r--lib/prado/framework/Data/SqlMap/DataMapper/TPropertyAccess.php153
-rw-r--r--lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapCache.php290
-rw-r--r--lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapException.php110
-rw-r--r--lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapPagedList.php206
-rw-r--r--lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapTypeHandlerRegistry.php189
-rw-r--r--lib/prado/framework/Data/SqlMap/DataMapper/messages.txt66
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/IMappedStatement.php80
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TCachingStatement.php106
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TDeleteMappedStatement.php22
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TInsertMappedStatement.php47
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TMappedStatement.php1236
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TPreparedCommand.php66
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TPreparedStatement.php54
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TPreparedStatementFactory.php47
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TSelectMappedStatement.php34
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TSimpleDynamicSql.php38
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TStaticSql.php34
-rw-r--r--lib/prado/framework/Data/SqlMap/Statements/TUpdateMappedStatement.php47
-rw-r--r--lib/prado/framework/Data/SqlMap/TSqlMapConfig.php179
-rw-r--r--lib/prado/framework/Data/SqlMap/TSqlMapGateway.php259
-rw-r--r--lib/prado/framework/Data/SqlMap/TSqlMapManager.php272
-rw-r--r--lib/prado/framework/Data/TDataSourceConfig.php165
-rw-r--r--lib/prado/framework/Data/TDbCommand.php306
-rw-r--r--lib/prado/framework/Data/TDbConnection.php680
-rw-r--r--lib/prado/framework/Data/TDbDataReader.php223
-rw-r--r--lib/prado/framework/Data/TDbTransaction.php110
95 files changed, 18521 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');
+ }
+}
diff --git a/lib/prado/framework/Data/Common/Mssql/TMssqlCommandBuilder.php b/lib/prado/framework/Data/Common/Mssql/TMssqlCommandBuilder.php
new file mode 100644
index 0000000..e3f1f5c
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Mssql/TMssqlCommandBuilder.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ * TMsssqlCommandBuilder 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.Common
+ */
+
+Prado::using('System.Data.Common.TDbCommandBuilder');
+
+/**
+ * TMssqlCommandBuilder provides specifics methods to create limit/offset query commands
+ * for MSSQL servers.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TMssqlCommandBuilder extends TDbCommandBuilder
+{
+ /**
+ * Overrides parent implementation. Uses "SELECT @@Identity".
+ * @return integer last insert id, null if none is found.
+ */
+ public function getLastInsertID()
+ {
+ foreach($this->getTableInfo()->getColumns() as $column)
+ {
+ if($column->hasSequence())
+ {
+ $command = $this->getDbConnection()->createCommand('SELECT @@Identity');
+ return intval($command->queryScalar());
+ }
+ }
+ }
+
+ /**
+ * Overrides parent implementation. Alters the sql to apply $limit and $offset.
+ * The idea for limit with offset is done by modifying the sql on the fly
+ * with numerous assumptions on the structure of the sql string.
+ * The modification is done with reference to the notes from
+ * http://troels.arvin.dk/db/rdbms/#select-limit-offset
+ *
+ * <code>
+ * SELECT * FROM (
+ * SELECT TOP n * FROM (
+ * SELECT TOP z columns -- (z=n+skip)
+ * FROM tablename
+ * ORDER BY key ASC
+ * ) AS FOO ORDER BY key DESC -- ('FOO' may be anything)
+ * ) AS BAR ORDER BY key ASC -- ('BAR' may be anything)
+ * </code>
+ *
+ * <b>Regular expressions are used to alter the SQL query. The resulting SQL query
+ * may be malformed for complex queries.</b> The following restrictions apply
+ *
+ * <ul>
+ * <li>
+ * In particular, <b>commas</b> should <b>NOT</b>
+ * be used as part of the ordering expression or identifier. Commas must only be
+ * used for separating the ordering clauses.
+ * </li>
+ * <li>
+ * In the ORDER BY clause, the column name should NOT be be qualified
+ * with a table name or view name. Alias the column names or use column index.
+ * </li>
+ * <li>
+ * No clauses should follow the ORDER BY clause, e.g. no COMPUTE or FOR clauses.
+ * </li>
+ * </ul>
+ *
+ * @param string SQL query string.
+ * @param integer maximum number of rows, -1 to ignore limit.
+ * @param integer row offset, -1 to ignore offset.
+ * @return string SQL with limit and offset.
+ */
+ public function applyLimitOffset($sql, $limit=-1, $offset=-1)
+ {
+ $limit = $limit!==null ? intval($limit) : -1;
+ $offset = $offset!==null ? intval($offset) : -1;
+ if ($limit > 0 && $offset <= 0) //just limit
+ $sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $limit", $sql);
+ else if($limit > 0 && $offset > 0)
+ $sql = $this->rewriteLimitOffsetSql($sql, $limit,$offset);
+ return $sql;
+ }
+
+ /**
+ * Rewrite sql to apply $limit > and $offset > 0 for MSSQL database.
+ * See http://troels.arvin.dk/db/rdbms/#select-limit-offset
+ * @param string sql query
+ * @param integer $limit > 0
+ * @param integer $offset > 0
+ * @return sql modified sql query applied with limit and offset.
+ */
+ protected function rewriteLimitOffsetSql($sql, $limit, $offset)
+ {
+ $fetch = $limit+$offset;
+ $sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $fetch", $sql);
+ $ordering = $this->findOrdering($sql);
+
+ $orginalOrdering = $this->joinOrdering($ordering);
+ $reverseOrdering = $this->joinOrdering($this->reverseDirection($ordering));
+ $sql = "SELECT * FROM (SELECT TOP {$limit} * FROM ($sql) as [__inner top table__] {$reverseOrdering}) as [__outer top table__] {$orginalOrdering}";
+ return $sql;
+ }
+
+ /**
+ * Base on simplified syntax http://msdn2.microsoft.com/en-us/library/aa259187(SQL.80).aspx
+ *
+ * @param string $sql
+ * @return array ordering expression as key and ordering direction as value
+ */
+ protected function findOrdering($sql)
+ {
+ if(!preg_match('/ORDER BY/i', $sql))
+ return array();
+ $matches=array();
+ $ordering=array();
+ preg_match_all('/(ORDER BY)[\s"\[](.*)(ASC|DESC)?(?:[\s"\[]|$|COMPUTE|FOR)/i', $sql, $matches);
+ if(count($matches)>1 && count($matches[2]) > 0)
+ {
+ $parts = explode(',', $matches[2][0]);
+ foreach($parts as $part)
+ {
+ $subs=array();
+ if(preg_match_all('/(.*)[\s"\]](ASC|DESC)$/i', trim($part), $subs))
+ {
+ if(count($subs) > 1 && count($subs[2]) > 0)
+ {
+ $ordering[$subs[1][0]] = $subs[2][0];
+ }
+ //else what?
+ }
+ else
+ $ordering[trim($part)] = 'ASC';
+ }
+ }
+ return $ordering;
+ }
+
+ /**
+ * @param array ordering obtained from findOrdering()
+ * @return string concat the orderings
+ */
+ protected function joinOrdering($orders)
+ {
+ if(count($orders)>0)
+ {
+ $str=array();
+ foreach($orders as $column => $direction)
+ $str[] = $column.' '.$direction;
+ return 'ORDER BY '.implode(', ', $str);
+ }
+ }
+
+ /**
+ * @param array original ordering
+ * @return array ordering with reversed direction.
+ */
+ protected function reverseDirection($orders)
+ {
+ foreach($orders as $column => $direction)
+ $orders[$column] = strtolower(trim($direction))==='desc' ? 'ASC' : 'DESC';
+ return $orders;
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Mssql/TMssqlMetaData.php b/lib/prado/framework/Data/Common/Mssql/TMssqlMetaData.php
new file mode 100644
index 0000000..d0fc167
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Mssql/TMssqlMetaData.php
@@ -0,0 +1,290 @@
+<?php
+/**
+ * TMssqlMetaData 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.Common.Mssql
+ */
+
+/**
+ * Load the base TDbMetaData class.
+ */
+Prado::using('System.Data.Common.TDbMetaData');
+Prado::using('System.Data.Common.Mssql.TMssqlTableInfo');
+
+/**
+ * TMssqlMetaData loads MSSQL database table and column information.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Mssql
+ * @since 3.1
+ */
+class TMssqlMetaData extends TDbMetaData
+{
+ /**
+ * @return string TDbTableInfo class name.
+ */
+ protected function getTableInfoClass()
+ {
+ return 'TMssqlTableInfo';
+ }
+
+ /**
+ * Quotes a table name for use in a query.
+ * @param string $name table name
+ * @return string the properly quoted table name
+ */
+ public function quoteTableName($name)
+ {
+ return parent::quoteTableName($name, '[', ']');
+ }
+
+ /**
+ * Quotes a column name for use in a query.
+ * @param string $name column name
+ * @return string the properly quoted column name
+ */
+ public function quoteColumnName($name)
+ {
+ return parent::quoteColumnName($name, '[', ']');
+ }
+
+ /**
+ * Quotes a column alias for use in a query.
+ * @param string $name column alias
+ * @return string the properly quoted column alias
+ */
+ public function quoteColumnAlias($name)
+ {
+ return parent::quoteColumnAlias($name, '"', '"');
+ }
+
+ /**
+ * Get the column definitions for given table.
+ * @param string table name.
+ * @return TMssqlTableInfo table information.
+ */
+ protected function createTableInfo($table)
+ {
+ list($catalogName,$schemaName,$tableName) = $this->getCatalogSchemaTableName($table);
+ $this->getDbConnection()->setActive(true);
+ $sql = <<<EOD
+ SELECT t.*,
+ c.*,
+ columnproperty(object_id(c.table_schema + '.' + c.table_name), c.column_name,'IsIdentity') as IsIdentity
+ FROM INFORMATION_SCHEMA.TABLES t,
+ INFORMATION_SCHEMA.COLUMNS c
+ WHERE t.table_name = c.table_name
+ AND t.table_name = :table
+EOD;
+ if($schemaName!==null)
+ $sql .= ' AND t.table_schema = :schema';
+ if($catalogName!==null)
+ $sql .= ' AND t.table_catalog = :catalog';
+
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $tableName);
+ if($schemaName!==null)
+ $command->bindValue(':schema', $schemaName);
+ if($catalogName!==null)
+ $command->bindValue(':catalog', $catalogName);
+
+ $tableInfo=null;
+ foreach($command->query() as $col)
+ {
+ if($tableInfo===null)
+ $tableInfo = $this->createNewTableInfo($col);
+ $this->processColumn($tableInfo,$col);
+ }
+ if($tableInfo===null)
+ throw new TDbException('dbmetadata_invalid_table_view', $table);
+ return $tableInfo;
+ }
+
+ /**
+ * @param string table name
+ * @return array tuple($catalogName,$schemaName,$tableName)
+ */
+ protected function getCatalogSchemaTableName($table)
+ {
+ //remove possible delimiters
+ $result = explode('.', preg_replace('/\[|\]|"/', '', $table));
+ if(count($result)===1)
+ return array(null,null,$result[0]);
+ if(count($result)===2)
+ return array(null,$result[0],$result[1]);
+ if(count($result)>2)
+ return array($result[0],$result[1],$result[2]);
+ }
+
+ /**
+ * @param TMssqlTableInfo table information.
+ * @param array column information.
+ */
+ protected function processColumn($tableInfo, $col)
+ {
+ $columnId = $col['COLUMN_NAME'];
+
+ $info['ColumnName'] = "[$columnId]"; //quote the column names!
+ $info['ColumnId'] = $columnId;
+ $info['ColumnIndex'] = intval($col['ORDINAL_POSITION'])-1; //zero-based index
+ if($col['IS_NULLABLE']!=='NO')
+ $info['AllowNull'] = true;
+ if($col['COLUMN_DEFAULT']!==null)
+ $info['DefaultValue'] = $col['COLUMN_DEFAULT'];
+
+ if(in_array($columnId, $tableInfo->getPrimaryKeys()))
+ $info['IsPrimaryKey'] = true;
+ if($this->isForeignKeyColumn($columnId, $tableInfo))
+ $info['IsForeignKey'] = true;
+
+ if($col['IsIdentity']==='1')
+ $info['AutoIncrement'] = true;
+ $info['DbType'] = $col['DATA_TYPE'];
+ if($col['CHARACTER_MAXIMUM_LENGTH']!==null)
+ $info['ColumnSize'] = intval($col['CHARACTER_MAXIMUM_LENGTH']);
+ if($col['NUMERIC_PRECISION'] !== null)
+ $info['NumericPrecision'] = intval($col['NUMERIC_PRECISION']);
+ if($col['NUMERIC_SCALE']!==null)
+ $info['NumericScale'] = intval($col['NUMERIC_SCALE']);
+ $tableInfo->Columns[$columnId] = new TMssqlTableColumn($info);
+ }
+
+ /**
+ * @param string table schema name
+ * @param string table name.
+ * @return TMssqlTableInfo
+ */
+ protected function createNewTableInfo($col)
+ {
+ $info['CatalogName'] = $col['TABLE_CATALOG'];
+ $info['SchemaName'] = $col['TABLE_SCHEMA'];
+ $info['TableName'] = $col['TABLE_NAME'];
+ if($col['TABLE_TYPE']==='VIEW')
+ $info['IsView'] = true;
+ list($primary, $foreign) = $this->getConstraintKeys($col);
+ $class = $this->getTableInfoClass();
+ return new $class($info,$primary,$foreign);
+ }
+
+ /**
+ * Gets the primary and foreign key column details for the given table.
+ * @param string schema name
+ * @param string table name.
+ * @return array tuple ($primary, $foreign)
+ */
+ protected function getConstraintKeys($col)
+ {
+ $sql = <<<EOD
+ SELECT k.column_name field_name
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
+ LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS c
+ ON k.table_name = c.table_name
+ AND k.constraint_name = c.constraint_name
+ WHERE k.constraint_catalog = DB_NAME()
+ AND
+ c.constraint_type ='PRIMARY KEY'
+ AND k.table_name = :table
+EOD;
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $col['TABLE_NAME']);
+ $primary = array();
+ foreach($command->query()->readAll() as $field)
+ $primary[] = $field['field_name'];
+ $foreign = $this->getForeignConstraints($col);
+ return array($primary,$foreign);
+ }
+
+ /**
+ * Gets foreign relationship constraint keys and table name
+ * @param string database name
+ * @param string table name
+ * @return array foreign relationship table name and keys.
+ */
+ protected function getForeignConstraints($col)
+ {
+ //From http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx
+ $sql = <<<EOD
+ SELECT
+ KCU1.CONSTRAINT_NAME AS 'FK_CONSTRAINT_NAME'
+ , KCU1.TABLE_NAME AS 'FK_TABLE_NAME'
+ , KCU1.COLUMN_NAME AS 'FK_COLUMN_NAME'
+ , KCU1.ORDINAL_POSITION AS 'FK_ORDINAL_POSITION'
+ , KCU2.CONSTRAINT_NAME AS 'UQ_CONSTRAINT_NAME'
+ , KCU2.TABLE_NAME AS 'UQ_TABLE_NAME'
+ , KCU2.COLUMN_NAME AS 'UQ_COLUMN_NAME'
+ , KCU2.ORDINAL_POSITION AS 'UQ_ORDINAL_POSITION'
+ FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC
+ JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU1
+ ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG
+ AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA
+ AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
+ JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU2
+ ON KCU2.CONSTRAINT_CATALOG =
+ RC.UNIQUE_CONSTRAINT_CATALOG
+ AND KCU2.CONSTRAINT_SCHEMA =
+ RC.UNIQUE_CONSTRAINT_SCHEMA
+ AND KCU2.CONSTRAINT_NAME =
+ RC.UNIQUE_CONSTRAINT_NAME
+ AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION
+ WHERE KCU1.TABLE_NAME = :table
+EOD;
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $col['TABLE_NAME']);
+ $fkeys=array();
+ $catalogSchema = "[{$col['TABLE_CATALOG']}].[{$col['TABLE_SCHEMA']}]";
+ foreach($command->query() as $info)
+ {
+ $fkeys[$info['FK_CONSTRAINT_NAME']]['keys'][$info['FK_COLUMN_NAME']] = $info['UQ_COLUMN_NAME'];
+ $fkeys[$info['FK_CONSTRAINT_NAME']]['table'] = $info['UQ_TABLE_NAME'];
+ }
+ return count($fkeys) > 0 ? array_values($fkeys) : $fkeys;
+ }
+
+ /**
+ * @param string column name.
+ * @param TPgsqlTableInfo table information.
+ * @return boolean true if column is a foreign key.
+ */
+ protected function isForeignKeyColumn($columnId, $tableInfo)
+ {
+ foreach($tableInfo->getForeignKeys() as $fk)
+ {
+ if(in_array($columnId, array_keys($fk['keys'])))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns all table names in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
+ * If not empty, the returned table names will be prefixed with the schema name.
+ * @return array all table names in the database.
+ */
+ public function findTableNames($schema='dbo')
+ {
+ $condition="TABLE_TYPE='BASE TABLE'";
+ $sql=<<<EOD
+SELECT TABLE_NAME, TABLE_SCHEMA FROM [INFORMATION_SCHEMA].[TABLES]
+WHERE TABLE_SCHEMA=:schema AND $condition
+EOD;
+ $command=$this->getDbConnection()->createCommand($sql);
+ $command->bindParam(":schema", $schema);
+ $rows=$command->queryAll();
+ $names=array();
+ foreach ($rows as $row)
+ {
+ if ($schema == self::DEFAULT_SCHEMA)
+ $names[]=$row['TABLE_NAME'];
+ else
+ $names[]=$schema.'.'.$row['TABLE_SCHEMA'].'.'.$row['TABLE_NAME'];
+ }
+
+ return $names;
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Mssql/TMssqlTableColumn.php b/lib/prado/framework/Data/Common/Mssql/TMssqlTableColumn.php
new file mode 100644
index 0000000..a02ebf0
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Mssql/TMssqlTableColumn.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * TMssqlTableColumn 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.Common.Mssql
+ */
+
+/**
+ * Load common TDbTableCommon class.
+ */
+Prado::using('System.Data.Common.TDbTableColumn');
+
+/**
+ * Describes the column metadata of the schema for a Mssql database table.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Mssql
+ * @since 3.1
+ */
+class TMssqlTableColumn extends TDbTableColumn
+{
+ private static $types = array();
+
+ /**
+ * Overrides parent implementation, returns PHP type from the db type.
+ * @return boolean derived PHP primitive type from the column db type.
+ */
+ public function getPHPType()
+ {
+
+ return 'string';
+ }
+
+ /**
+ * @return boolean true if the column has identity (auto-increment)
+ */
+ public function getAutoIncrement()
+ {
+ return $this->getInfo('AutoIncrement',false);
+ }
+
+ /**
+ * @return boolean true if auto increments.
+ */
+ public function hasSequence()
+ {
+ return $this->getAutoIncrement();
+ }
+
+ /**
+ * @return boolean true if db type is 'timestamp'.
+ */
+ public function getIsExcluded()
+ {
+ return strtolower($this->getDbType())==='timestamp';
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Mssql/TMssqlTableInfo.php b/lib/prado/framework/Data/Common/Mssql/TMssqlTableInfo.php
new file mode 100644
index 0000000..bee4730
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Mssql/TMssqlTableInfo.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * TMssqlTableInfo 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.Common.Mssql
+ */
+
+/**
+ * Loads the base TDbTableInfo class and TMssqlTableColumn class.
+ */
+Prado::using('System.Data.Common.TDbTableInfo');
+Prado::using('System.Data.Common.Mssql.TMssqlTableColumn');
+
+/**
+ * TMssqlTableInfo class provides additional table information for Mssql database.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Mssql
+ * @since 3.1
+ */
+class TMssqlTableInfo extends TDbTableInfo
+{
+ /**
+ * @return string name of the schema this column belongs to.
+ */
+ public function getSchemaName()
+ {
+ return $this->getInfo('SchemaName');
+ }
+
+ /**
+ * @return string catalog name (database name)
+ */
+ public function getCatalogName()
+ {
+ return $this->getInfo('CatalogName');
+ }
+
+ /**
+ * @return string full name of the table, database dependent.
+ */
+ public function getTableFullName()
+ {
+ //MSSQL alway returns the catalog, schem and table names.
+ return '['.$this->getCatalogName().'].['.$this->getSchemaName().'].['.$this->getTableName().']';
+ }
+
+ /**
+ * @param TDbConnection database connection.
+ * @return TDbCommandBuilder new command builder
+ */
+ public function createCommandBuilder($connection)
+ {
+ Prado::using('System.Data.Common.Mssql.TMssqlCommandBuilder');
+ return new TMssqlCommandBuilder($connection,$this);
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Mysql/TMysqlCommandBuilder.php b/lib/prado/framework/Data/Common/Mysql/TMysqlCommandBuilder.php
new file mode 100644
index 0000000..b5bdc28
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Mysql/TMysqlCommandBuilder.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * TMysqlCommandBuilder 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.Common
+ */
+
+Prado::using('System.Data.Common.TDbCommandBuilder');
+
+/**
+ * TMysqlCommandBuilder implements default TDbCommandBuilder
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TMysqlCommandBuilder extends TDbCommandBuilder
+{
+}
+
diff --git a/lib/prado/framework/Data/Common/Mysql/TMysqlMetaData.php b/lib/prado/framework/Data/Common/Mysql/TMysqlMetaData.php
new file mode 100644
index 0000000..e7747b7
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Mysql/TMysqlMetaData.php
@@ -0,0 +1,405 @@
+<?php
+/**
+ * TMysqlMetaData 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.Common.Mysql
+ */
+
+/**
+ * Load the base TDbMetaData class.
+ */
+Prado::using('System.Data.Common.TDbMetaData');
+Prado::using('System.Data.Common.Mysql.TMysqlTableInfo');
+
+/**
+ * TMysqlMetaData loads Mysql version 4.1.x and 5.x database table and column information.
+ *
+ * For Mysql version 4.1.x, PHP 5.1.3 or later is required.
+ * See http://netevil.org/node.php?nid=795&SC=1
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Mysql
+ * @since 3.1
+ */
+class TMysqlMetaData extends TDbMetaData
+{
+ private $_serverVersion=0;
+
+ /**
+ * @return string TDbTableInfo class name.
+ */
+ protected function getTableInfoClass()
+ {
+ return 'TMysqlTableInfo';
+ }
+
+ /**
+ * Quotes a table name for use in a query.
+ * @param string $name table name
+ * @return string the properly quoted table name
+ */
+ public function quoteTableName($name)
+ {
+ return parent::quoteTableName($name, '`', '`');
+ }
+
+ /**
+ * Quotes a column name for use in a query.
+ * @param string $name column name
+ * @return string the properly quoted column name
+ */
+ public function quoteColumnName($name)
+ {
+ return parent::quoteColumnName($name, '`', '`');
+ }
+
+ /**
+ * Quotes a column alias for use in a query.
+ * @param string $name column alias
+ * @return string the properly quoted column alias
+ */
+ public function quoteColumnAlias($name)
+ {
+ return parent::quoteColumnAlias($name, '`', '`');
+ }
+
+ /**
+ * Get the column definitions for given table.
+ * @param string table name.
+ * @return TMysqlTableInfo table information.
+ */
+ protected function createTableInfo($table)
+ {
+ list($schemaName,$tableName) = $this->getSchemaTableName($table);
+ $find = $schemaName===null ? "`{$tableName}`" : "`{$schemaName}`.`{$tableName}`";
+ $colCase = $this->getDbConnection()->getColumnCase();
+ if($colCase != TDbColumnCaseMode::Preserved)
+ $this->getDbConnection()->setColumnCase('Preserved');
+ $this->getDbConnection()->setActive(true);
+ $sql = "SHOW FULL FIELDS FROM {$find}";
+ $command = $this->getDbConnection()->createCommand($sql);
+ $tableInfo = $this->createNewTableInfo($table);
+ $index=0;
+ foreach($command->query() as $col)
+ {
+ $col['index'] = $index++;
+ $this->processColumn($tableInfo,$col);
+ }
+ if($index===0)
+ throw new TDbException('dbmetadata_invalid_table_view', $table);
+ if($colCase != TDbColumnCaseMode::Preserved)
+ $this->getDbConnection()->setColumnCase($colCase);
+ return $tableInfo;
+ }
+
+ /**
+ * @return float server version.
+ */
+ protected function getServerVersion()
+ {
+ if(!$this->_serverVersion)
+ {
+ $version = $this->getDbConnection()->getAttribute(PDO::ATTR_SERVER_VERSION);
+ $digits=array();
+ preg_match('/(\d+)\.(\d+)\.(\d+)/', $version, $digits);
+ $this->_serverVersion=floatval($digits[1].'.'.$digits[2].$digits[3]);
+ }
+ return $this->_serverVersion;
+ }
+
+ /**
+ * @param TMysqlTableInfo table information.
+ * @param array column information.
+ */
+ protected function processColumn($tableInfo, $col)
+ {
+ $columnId = $col['Field'];
+
+ $info['ColumnName'] = "`$columnId`"; //quote the column names!
+ $info['ColumnId'] = $columnId;
+ $info['ColumnIndex'] = $col['index'];
+ if($col['Null']==='YES')
+ $info['AllowNull'] = true;
+ if(is_int(strpos(strtolower($col['Extra']), 'auto_increment')))
+ $info['AutoIncrement']=true;
+ if($col['Default']!=="")
+ $info['DefaultValue'] = $col['Default'];
+
+ if($col['Key']==='PRI' || in_array($columnId, $tableInfo->getPrimaryKeys()))
+ $info['IsPrimaryKey'] = true;
+ if($this->isForeignKeyColumn($columnId, $tableInfo))
+ $info['IsForeignKey'] = true;
+
+ $info['DbType'] = $col['Type'];
+ $match=array();
+ //find SET/ENUM values, column size, precision, and scale
+ if(preg_match('/\((.*)\)/', $col['Type'], $match))
+ {
+ $info['DbType']= preg_replace('/\(.*\)/', '', $col['Type']);
+
+ //find SET/ENUM values
+ if($this->isEnumSetType($info['DbType']))
+ $info['DbTypeValues'] = preg_split("/[',]/S", $match[1], -1, PREG_SPLIT_NO_EMPTY);
+
+ //find column size, precision and scale
+ $pscale = array();
+ if(preg_match('/(\d+)(?:,(\d+))?+/', $match[1], $pscale))
+ {
+ if($this->isPrecisionType($info['DbType']))
+ {
+ $info['NumericPrecision'] = intval($pscale[1]);
+ if(count($pscale) > 2)
+ $info['NumericScale'] = intval($pscale[2]);
+ }
+ else
+ $info['ColumnSize'] = intval($pscale[1]);
+ }
+ }
+
+ $tableInfo->Columns[$columnId] = new TMysqlTableColumn($info);
+ }
+
+ /**
+ * @return boolean true if column type if "numeric", "interval" or begins with "time".
+ */
+ protected function isPrecisionType($type)
+ {
+ $type = strtolower(trim($type));
+ return $type==='decimal' || $type==='dec'
+ || $type==='float' || $type==='double'
+ || $type==='double precision' || $type==='real';
+ }
+
+ /**
+ * @return boolean true if column type if "enum" or "set".
+ */
+ protected function isEnumSetType($type)
+ {
+ $type = strtolower(trim($type));
+ return $type==='set' || $type==='enum';
+ }
+
+ /**
+ * @param string table name, may be quoted with back-ticks and may contain database name.
+ * @return array tuple ($schema,$table), $schema may be null.
+ * @throws TDbException when table name contains invalid identifier bytes.
+ */
+ protected function getSchemaTableName($table)
+ {
+ //remove the back ticks and separate out the "database.table"
+ $result = explode('.', str_replace('`', '', $table));
+ foreach($result as $name)
+ {
+ if(!$this->isValidIdentifier($name))
+ {
+ $ref = 'http://dev.mysql.com/doc/refman/5.0/en/identifiers.html';
+ throw new TDbException('dbcommon_invalid_identifier_name', $table, $ref);
+ }
+ }
+ return count($result) > 1 ? $result : array(null, $result[0]);
+ }
+
+ /**
+ * http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
+ * @param string identifier name
+ * @param boolean true if valid identifier.
+ */
+ protected function isValidIdentifier($name)
+ {
+ return !preg_match('#/|\\|.|\x00|\xFF#', $name);
+ }
+
+ /**
+ * @param string table schema name
+ * @param string table name.
+ * @return TMysqlTableInfo
+ */
+ protected function createNewTableInfo($table)
+ {
+ list($schemaName,$tableName) = $this->getSchemaTableName($table);
+ $info['SchemaName'] = $schemaName;
+ $info['TableName'] = $tableName;
+ if($this->getIsView($schemaName,$tableName))
+ $info['IsView'] = true;
+ list($primary, $foreign) = $this->getConstraintKeys($schemaName, $tableName);
+ $class = $this->getTableInfoClass();
+ return new $class($info,$primary,$foreign);
+ }
+
+ /**
+ * For MySQL version 5.0.1 or later we can use SHOW FULL TABLES
+ * http://dev.mysql.com/doc/refman/5.0/en/show-tables.html
+ *
+ * For MySQL version 5.0.1 or ealier, this always return false.
+ * @param string database name, null to use default connection database.
+ * @param string table or view name.
+ * @return boolean true if is view, false otherwise.
+ * @throws TDbException if table or view does not exist.
+ */
+ protected function getIsView($schemaName,$tableName)
+ {
+ if($this->getServerVersion()<5.01)
+ return false;
+ if($schemaName!==null)
+ $sql = "SHOW FULL TABLES FROM `{$schemaName}` LIKE :table";
+ else
+ $sql = "SHOW FULL TABLES LIKE :table";
+
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $tableName);
+ try
+ {
+ return count($result = $command->queryRow()) > 0 && $result['Table_type']==='VIEW';
+ }
+ catch(TDbException $e)
+ {
+ $table = $schemaName===null?$tableName:$schemaName.'.'.$tableName;
+ throw new TDbException('dbcommon_invalid_table_name',$table,$e->getMessage());
+ }
+ }
+
+ /**
+ * Gets the primary and foreign key column details for the given table.
+ * @param string schema name
+ * @param string table name.
+ * @return array tuple ($primary, $foreign)
+ */
+ protected function getConstraintKeys($schemaName, $tableName)
+ {
+ $table = $schemaName===null ? "`{$tableName}`" : "`{$schemaName}`.`{$tableName}`";
+ $sql = "SHOW INDEX FROM {$table}";
+ $command = $this->getDbConnection()->createCommand($sql);
+ $primary = array();
+ foreach($command->query() as $row)
+ {
+ if($row['Key_name']==='PRIMARY')
+ $primary[] = $row['Column_name'];
+ }
+ // MySQL version was increased to >=5.1.21 instead of 5.x
+ // due to a MySQL bug (http://bugs.mysql.com/bug.php?id=19588)
+ if($this->getServerVersion() >= 5.121)
+ $foreign = $this->getForeignConstraints($schemaName,$tableName);
+ else
+ $foreign = $this->findForeignConstraints($schemaName,$tableName);
+ return array($primary,$foreign);
+ }
+
+ /**
+ * Gets foreign relationship constraint keys and table name
+ * @param string database name
+ * @param string table name
+ * @return array foreign relationship table name and keys.
+ */
+ protected function getForeignConstraints($schemaName, $tableName)
+ {
+ $andSchema = $schemaName !== null ? 'AND TABLE_SCHEMA LIKE :schema' : 'AND TABLE_SCHEMA LIKE DATABASE()';
+ $sql = <<<EOD
+ SELECT
+ CONSTRAINT_NAME as con,
+ COLUMN_NAME as col,
+ REFERENCED_TABLE_SCHEMA as fkschema,
+ REFERENCED_TABLE_NAME as fktable,
+ REFERENCED_COLUMN_NAME as fkcol
+ FROM
+ `INFORMATION_SCHEMA`.`KEY_COLUMN_USAGE`
+ WHERE
+ REFERENCED_TABLE_NAME IS NOT NULL
+ AND TABLE_NAME LIKE :table
+ $andSchema
+EOD;
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $tableName);
+ if($schemaName!==null)
+ $command->bindValue(':schema', $schemaName);
+ $fkeys=array();
+ foreach($command->query() as $col)
+ {
+ $fkeys[$col['con']]['keys'][$col['col']] = $col['fkcol'];
+ $fkeys[$col['con']]['table'] = $col['fktable'];
+ }
+ return count($fkeys) > 0 ? array_values($fkeys) : $fkeys;
+ }
+
+ /**
+ * @param string database name
+ * @param string table name
+ * @return string SQL command to create the table.
+ * @throws TDbException if PHP version is less than 5.1.3
+ */
+ protected function getShowCreateTable($schemaName, $tableName)
+ {
+ if(version_compare(PHP_VERSION,'5.1.3','<'))
+ throw new TDbException('dbmetadata_requires_php_version', 'Mysql 4.1.x', '5.1.3');
+
+ //See http://netevil.org/node.php?nid=795&SC=1
+ $this->getDbConnection()->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
+ if($schemaName!==null)
+ $sql = "SHOW CREATE TABLE `{$schemaName}`.`{$tableName}`";
+ else
+ $sql = "SHOW CREATE TABLE `{$tableName}`";
+ $command = $this->getDbConnection()->createCommand($sql);
+ $result = $command->queryRow();
+ return isset($result['Create Table']) ? $result['Create Table'] : (isset($result['Create View']) ? $result['Create View'] : '');
+ }
+
+ /**
+ * Extract foreign key constraints by extracting the contraints from SHOW CREATE TABLE result.
+ * @param string database name
+ * @param string table name
+ * @return array foreign relationship table name and keys.
+ */
+ protected function findForeignConstraints($schemaName, $tableName)
+ {
+ $sql = $this->getShowCreateTable($schemaName, $tableName);
+ $matches =array();
+ $regexp = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+`?([^`]+)`?\s\(([^\)]+)\)/mi';
+ preg_match_all($regexp,$sql,$matches,PREG_SET_ORDER);
+ $foreign = array();
+ foreach($matches as $match)
+ {
+ $fields = array_map('trim',explode(',',str_replace('`','',$match[1])));
+ $fk_fields = array_map('trim',explode(',',str_replace('`','',$match[3])));
+ $keys=array();
+ foreach($fields as $k=>$v)
+ $keys[$v] = $fk_fields[$k];
+ $foreign[] = array('keys' => $keys, 'table' => trim($match[2]));
+ }
+ return $foreign;
+ }
+
+ /**
+ * @param string column name.
+ * @param TPgsqlTableInfo table information.
+ * @return boolean true if column is a foreign key.
+ */
+ protected function isForeignKeyColumn($columnId, $tableInfo)
+ {
+ foreach($tableInfo->getForeignKeys() as $fk)
+ {
+ if(in_array($columnId, array_keys($fk['keys'])))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns all table names in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
+ * If not empty, the returned table names will be prefixed with the schema name.
+ * @return array all table names in the database.
+ */
+ public function findTableNames($schema='')
+ {
+ if($schema==='')
+ return $this->getDbConnection()->createCommand('SHOW TABLES')->queryColumn();
+ $names=$this->getDbConnection()->createCommand('SHOW TABLES FROM '.$this->quoteTableName($schema))->queryColumn();
+ foreach($names as &$name)
+ $name=$schema.'.'.$name;
+ return $names;
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Mysql/TMysqlTableColumn.php b/lib/prado/framework/Data/Common/Mysql/TMysqlTableColumn.php
new file mode 100644
index 0000000..5b84f80
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Mysql/TMysqlTableColumn.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * TMysqlTableColumn 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.Common.Mysql
+ */
+
+/**
+ * Load common TDbTableCommon class.
+ */
+Prado::using('System.Data.Common.TDbTableColumn');
+
+/**
+ * Describes the column metadata of the schema for a Mysql database table.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Mysql
+ * @since 3.1
+ */
+class TMysqlTableColumn extends TDbTableColumn
+{
+ private static $types = array(
+ 'integer' => array('bit', 'tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint'),
+ 'boolean' => array('boolean', 'bool'),
+ 'float' => array('float', 'double', 'double precision', 'decimal', 'dec', 'numeric', 'fixed')
+ );
+
+ /**
+ * Overrides parent implementation, returns PHP type from the db type.
+ * @return boolean derived PHP primitive type from the column db type.
+ */
+ public function getPHPType()
+ {
+ $dbtype = trim(str_replace(array('unsigned', 'zerofill'),array('','',),strtolower($this->getDbType())));
+ if($dbtype==='tinyint' && $this->getColumnSize()===1)
+ return 'boolean';
+ foreach(self::$types as $type => $dbtypes)
+ {
+ if(in_array($dbtype, $dbtypes))
+ return $type;
+ }
+ return 'string';
+ }
+
+ /**
+ * @return boolean true if column will auto-increment when the column value is inserted as null.
+ */
+ public function getAutoIncrement()
+ {
+ return $this->getInfo('AutoIncrement', false);
+ }
+
+ /**
+ * @return boolean true if auto increment is true.
+ */
+ public function hasSequence()
+ {
+ return $this->getAutoIncrement();
+ }
+
+ public function getDbTypeValues()
+ {
+ return $this->getInfo('DbTypeValues');
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Mysql/TMysqlTableInfo.php b/lib/prado/framework/Data/Common/Mysql/TMysqlTableInfo.php
new file mode 100644
index 0000000..bc5dc4e
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Mysql/TMysqlTableInfo.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * TMysqlTableInfo 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.Common.Mysql
+ */
+
+/**
+ * Loads the base TDbTableInfo class and TMysqlTableColumn class.
+ */
+Prado::using('System.Data.Common.TDbTableInfo');
+Prado::using('System.Data.Common.Mysql.TMysqlTableColumn');
+
+/**
+ * TMysqlTableInfo class provides additional table information for MySQL database.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Mysql
+ * @since 3.1
+ */
+class TMysqlTableInfo extends TDbTableInfo
+{
+ /**
+ * @return string name of the schema this column belongs to.
+ */
+ public function getSchemaName()
+ {
+ return $this->getInfo('SchemaName');
+ }
+
+ /**
+ * @return string full name of the table, database dependent.
+ */
+ public function getTableFullName()
+ {
+ if(($schema=$this->getSchemaName())!==null)
+ return '`'.$schema.'`.`'.$this->getTableName().'`';
+ else
+ return '`'.$this->getTableName().'`';
+ }
+
+ /**
+ * @param TDbConnection database connection.
+ * @return TDbCommandBuilder new command builder
+ */
+ public function createCommandBuilder($connection)
+ {
+ Prado::using('System.Data.Common.Mysql.TMysqlCommandBuilder');
+ return new TMysqlCommandBuilder($connection,$this);
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Oracle/TOracleCommandBuilder.php b/lib/prado/framework/Data/Common/Oracle/TOracleCommandBuilder.php
new file mode 100644
index 0000000..ad4c9f4
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Oracle/TOracleCommandBuilder.php
@@ -0,0 +1,153 @@
+<?php
+
+/**
+ * TOracleCommandBuilder class file.
+ *
+ * @author Marcos Nobre <marconobre[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.Common
+ */
+
+Prado :: using('System.Data.Common.TDbCommandBuilder');
+
+/**
+ * TOracleCommandBuilder provides specifics methods to create limit/offset query commands
+ * for Oracle database.
+ *
+ * @author Marcos Nobre <marconobre[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TOracleCommandBuilder extends TDbCommandBuilder {
+
+ /**
+ * Overrides parent implementation. Only column of type text or character (and its variants)
+ * accepts the LIKE criteria.
+ * @param array list of column id for potential search condition.
+ * @param string string of keywords
+ * @return string SQL search condition matching on a set of columns.
+ */
+ public function getSearchExpression($fields, $keywords) {
+ $columns = array ();
+ foreach ($fields as $field) {
+ if ($this->isSearchableColumn($this->getTableInfo()->getColumn($field)))
+ $columns[] = $field;
+ }
+ return parent :: getSearchExpression($columns, $keywords);
+ }
+ /**
+ *
+ * @return boolean true if column can be used for LIKE searching.
+ */
+ protected function isSearchableColumn($column) {
+ $type = strtolower($column->getDbType());
+ return $type === 'character varying' || $type === 'varchar2' || $type === 'character' || $type === 'char' || $type === 'text';
+ }
+
+ /**
+ * Overrides parent implementation to use PostgreSQL's ILIKE instead of LIKE (case-sensitive).
+ * @param string column name.
+ * @param array keywords
+ * @return string search condition for all words in one column.
+ */
+ /*
+ *
+ * how Oracle don't implements ILIKE, this method won't be overrided
+ *
+ protected function getSearchCondition($column, $words)
+ {
+ $conditions=array();
+ foreach($words as $word)
+ $conditions[] = $column.' LIKE '.$this->getDbConnection()->quoteString('%'.$word.'%');
+ return '('.implode(' AND ', $conditions).')';
+ }
+ */
+
+ /**
+ * Overrides parent implementation to use Oracle way of get paginated RecordSet instead of using LIMIT sql clause.
+ * @param string SQL query string.
+ * @param integer maximum number of rows, -1 to ignore limit.
+ * @param integer row offset, -1 to ignore offset.
+ * @return string SQL with limit and offset in Oracle way.
+ */
+ public function applyLimitOffset($sql, $limit = -1, $offset = -1) {
+ if ((int) $limit <= 0 && (int) $offset <= 0)
+ return $sql;
+
+ $pradoNUMLIN = 'pradoNUMLIN';
+ $fieldsALIAS = 'xyz';
+
+ $nfimDaSQL = strlen($sql);
+ $nfimDoWhere = (strpos($sql, 'ORDER') !== false ? strpos($sql, 'ORDER') : $nfimDaSQL);
+ $niniDoSelect = strpos($sql, 'SELECT') + 6;
+ $nfimDoSelect = (strpos($sql, 'FROM') !== false ? strpos($sql, 'FROM') : $nfimDaSQL);
+
+ $WhereInSubSelect="";
+ if(strpos($sql, 'WHERE')!==false)
+ $WhereInSubSelect = "WHERE " .substr($sql, strpos($sql, 'WHERE')+5, $nfimDoWhere - $niniDoWhere);
+
+ $sORDERBY = '';
+ if (stripos($sql, 'ORDER') !== false) {
+ $p = stripos($sql, 'ORDER');
+ $sORDERBY = substr($sql, $p +8);
+ }
+
+ $fields = substr($sql, 0, $nfimDoSelect);
+ $fields = trim(substr($fields, $niniDoSelect));
+ $aliasedFields = ', ';
+
+ if (trim($fields) == '*') {
+ $aliasedFields = ", {$fieldsALIAS}.{$fields}";
+ $fields = '';
+ $arr = $this->getTableInfo()->getColumns();
+ foreach ($arr as $field) {
+ $fields .= strtolower($field->getColumnName()) . ', ';
+ }
+ $fields = str_replace('"', '', $fields);
+ $fields = trim($fields);
+ $fields = substr($fields, 0, strlen($fields) - 1);
+ } else {
+ if (strpos($fields, ',') !== false) {
+ $arr = $this->getTableInfo()->getColumns();
+ foreach ($arr as $field) {
+ $field = strtolower($field);
+ $existAS = str_ireplace(' as ', '-as-', $field);
+ if (strpos($existAS, '-as-') === false)
+ $aliasedFields .= "{$fieldsALIAS}." . trim($field) . ", ";
+ else
+ $aliasedFields .= "{$field}, ";
+ }
+ $aliasedFields = trim($aliasedFields);
+ $aliasedFields = substr($aliasedFields, 0, strlen($aliasedFields) - 1);
+ }
+ }
+ if ($aliasedFields == ', ')
+ $aliasedFields = " , $fieldsALIAS.* ";
+
+ /* ************************
+ $newSql = " SELECT $fields FROM ".
+ "( ".
+ " SELECT rownum as {$pradoNUMLIN} {$aliasedFields} FROM ".
+ " ($sql) {$fieldsALIAS} WHERE rownum <= {$limit} ".
+ ") WHERE {$pradoNUMLIN} >= {$offset} ";
+
+ ************************* */
+ $offset=(int)$offset;
+ $toReg = $offset + $limit ;
+ $fullTableName = $this->getTableInfo()->getTableFullName();
+ if (empty ($sORDERBY))
+ $sORDERBY="ROWNUM";
+
+ $newSql = " SELECT $fields FROM " .
+ "( " .
+ " SELECT ROW_NUMBER() OVER ( ORDER BY {$sORDERBY} ) -1 as {$pradoNUMLIN} {$aliasedFields} " .
+ " FROM {$fullTableName} {$fieldsALIAS} $WhereInSubSelect" .
+ ") nn " .
+ " WHERE nn.{$pradoNUMLIN} >= {$offset} AND nn.{$pradoNUMLIN} < {$toReg} ";
+ //echo $newSql."\n<br>\n";
+ return $newSql;
+ }
+
+}
diff --git a/lib/prado/framework/Data/Common/Oracle/TOracleMetaData.php b/lib/prado/framework/Data/Common/Oracle/TOracleMetaData.php
new file mode 100644
index 0000000..7fd3d1f
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Oracle/TOracleMetaData.php
@@ -0,0 +1,374 @@
+<?php
+/**
+ * TOracleMetaData class file.
+ *
+ * @author Marcos Nobre <marconobre[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.Common.Oracle
+ */
+
+/**
+ * Load the base TDbMetaData class.
+ */
+Prado::using('System.Data.Common.TDbMetaData');
+Prado::using('System.Data.Common.Oracle.TOracleTableInfo');
+Prado::using('System.Data.Common.Oracle.TOracleTableColumn');
+
+/**
+ * TOracleMetaData loads Oracle database table and column information.
+ *
+ * @author Marcos Nobre <marconobre[at]gmail[dot]com>
+ * @package System.Data.Common.Oracle
+ * @since 3.1
+ */
+class TOracleMetaData extends TDbMetaData
+{
+ private $_defaultSchema = 'system';
+
+
+ /**
+ * @return string TDbTableInfo class name.
+ */
+ protected function getTableInfoClass()
+ {
+ return 'TOracleTableInfo';
+ }
+
+ /**
+ * @param string default schema.
+ */
+ public function setDefaultSchema($schema)
+ {
+ $this->_defaultSchema=$schema;
+ }
+
+ /**
+ * @return string default schema.
+ */
+ public function getDefaultSchema()
+ {
+ return $this->_defaultSchema;
+ }
+
+ /**
+ * @param string table name with optional schema name prefix, uses default schema name prefix is not provided.
+ * @return array tuple as ($schemaName,$tableName)
+ */
+ protected function getSchemaTableName($table)
+ {
+ if(count($parts= explode('.', str_replace('"','',$table))) > 1)
+ return array($parts[0], $parts[1]);
+ else
+ return array($this->getDefaultSchema(),$parts[0]);
+ }
+
+ /**
+ * Get the column definitions for given table.
+ * @param string table name.
+ * @return TOracleTableInfo table information.
+ */
+ protected function createTableInfo($table)
+ {
+ list($schemaName,$tableName) = $this->getSchemaTableName($table);
+
+ // This query is made much more complex by the addition of the 'attisserial' field.
+ // The subquery to get that field checks to see if there is an internally dependent
+ // sequence on the field.
+ $sql =
+<<<EOD
+ SELECT
+ a.COLUMN_ID,
+ LOWER(a.COLUMN_NAME) as attname,
+ a.DATA_TYPE || DECODE( a.DATA_TYPE, 'NUMBER', '('||a.DATA_PRECISION||','||DATA_SCALE||')' , '') as type,
+ a.DATA_LENGTH as atttypmod,
+ DECODE(a.NULLABLE, 'Y', '0', '1') as attnotnull,
+ DECODE(a.DEFAULT_LENGTH, NULL, '0', '1') as atthasdef,
+ DATA_DEFAULT as adsrc,
+ '0' AS attisserial
+ FROM
+ ALL_TAB_COLUMNS a
+ WHERE
+ TABLE_NAME = '{$tableName}'
+ AND OWNER = '{$schemaName}'
+ ORDER BY a.COLUMN_ID
+EOD;
+ $this->getDbConnection()->setActive(true);
+ $this->getDbConnection()->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
+ $command = $this->getDbConnection()->createCommand($sql);
+ //$command->bindValue(':table', $tableName);
+ //$command->bindValue(':schema', $schemaName);
+ $tableInfo = $this->createNewTableInfo($schemaName, $tableName);
+ $index=0;
+ foreach($command->query() as $col)
+ {
+ $col['index'] = $index++;
+ $this->processColumn($tableInfo, $col);
+ }
+ if($index===0)
+ throw new TDbException('dbmetadata_invalid_table_view', $table);
+ return $tableInfo;
+ }
+
+ /**
+ * @param string table schema name
+ * @param string table name.
+ * @return TOracleTableInfo
+ */
+ protected function createNewTableInfo($schemaName,$tableName)
+ {
+ $info['SchemaName'] = $this->assertIdentifier($schemaName);
+ $info['TableName'] = $this->assertIdentifier($tableName);
+ $info['IsView'] = false;
+ if($this->getIsView($schemaName,$tableName)) $info['IsView'] = true;
+ list($primary, $foreign) = $this->getConstraintKeys($schemaName, $tableName);
+ $class = $this->getTableInfoClass();
+ return new $class($info,$primary,$foreign);
+ }
+
+ /**
+ * @param string table name, schema name or column name.
+ * @return string a valid identifier.
+ * @throws TDbException when table name contains a double quote (").
+ */
+ protected function assertIdentifier($name)
+ {
+ if(strpos($name, '"')!==false)
+ {
+ $ref = 'http://www.oracle.com';
+ throw new TDbException('dbcommon_invalid_identifier_name', $name, $ref);
+ }
+ return $name;
+ }
+
+ /**
+ * @param string table schema name
+ * @param string table name.
+ * @return boolean true if the table is a view.
+ */
+ protected function getIsView($schemaName,$tableName)
+ {
+ $this->getDbConnection()->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
+ $sql =
+<<<EOD
+ select OBJECT_TYPE
+ from ALL_OBJECTS
+ where OBJECT_NAME = '{$tableName}'
+ and OWNER = '{$schemaName}'
+EOD;
+ $this->getDbConnection()->setActive(true);
+ $command = $this->getDbConnection()->createCommand($sql);
+ //$command->bindValue(':schema',$schemaName);
+ //$command->bindValue(':table', $tableName);
+ return intval($command->queryScalar() === 'VIEW');
+ }
+
+ /**
+ * @param TOracleTableInfo table information.
+ * @param array column information.
+ */
+ protected function processColumn($tableInfo, $col)
+ {
+ $columnId = strtolower($col['attname']); //use column name as column Id
+
+ //$info['ColumnName'] = '"'.$columnId.'"'; //quote the column names!
+ $info['ColumnName'] = $columnId; //NOT quote the column names!
+ $info['ColumnId'] = $columnId;
+ $info['ColumnIndex'] = $col['index'];
+ if(! (bool)$col['attnotnull'] ) $info['AllowNull'] = true;
+ if(in_array($columnId, $tableInfo->getPrimaryKeys())) $info['IsPrimaryKey'] = true;
+ if($this->isForeignKeyColumn($columnId, $tableInfo)) $info['IsForeignKey'] = true;
+ if( (int)$col['atttypmod'] > 0 ) $info['ColumnSize'] = $col['atttypmod']; // - 4;
+ if( (bool)$col['atthasdef'] ) $info['DefaultValue'] = $col['adsrc'];
+ //
+ // For a while Oracle Tables has no associated AutoIncrement Triggers
+ //
+ /*
+ if( $col['attisserial'] )
+ {
+ if(($sequence = $this->getSequenceName($tableInfo, $col['adsrc']))!==null)
+ {
+ $info['SequenceName'] = $sequence;
+ unset($info['DefaultValue']);
+ }
+ }
+ */
+ $matches = array();
+ if(preg_match('/\((\d+)(?:,(\d+))?+\)/', $col['type'], $matches))
+ {
+ $info['DbType'] = preg_replace('/\(\d+(?:,\d+)?\)/','',$col['type']);
+ if($this->isPrecisionType($info['DbType']))
+ {
+ $info['NumericPrecision'] = intval($matches[1]);
+ if(count($matches) > 2)
+ $info['NumericScale'] = intval($matches[2]);
+ }
+ else
+ $info['ColumnSize'] = intval($matches[1]);
+ }
+ else
+ $info['DbType'] = $col['type'];
+ $tableInfo->Columns[$columnId] = new TOracleTableColumn($info);
+ }
+
+ /**
+ * @return string serial name if found, null otherwise.
+ */
+ protected function getSequenceName($tableInfo,$src)
+ {
+ $matches = array();
+ if(preg_match('/nextval\([^\']*\'([^\']+)\'[^\)]*\)/i',$src,$matches))
+ {
+ if(is_int(strpos($matches[1], '.')))
+ return $matches[1];
+ else
+ return $tableInfo->getSchemaName().'.'.$matches[1];
+ }
+ }
+
+ /**
+ * @return boolean true if column type if "numeric", "interval" or begins with "time".
+ */
+ protected function isPrecisionType($type)
+ {
+ $type = strtolower(trim($type));
+ return $type==='number'; // || $type==='interval' || strpos($type, 'time')===0;
+ }
+
+ /**
+ * Gets the primary and foreign key column details for the given table.
+ * @param string schema name
+ * @param string table name.
+ * @return array tuple ($primary, $foreign)
+ */
+ protected function getConstraintKeys($schemaName, $tableName)
+ {
+ $this->getDbConnection()->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
+// select decode( a.CONSTRAINT_TYPE, 'P', 'PRIMARY KEY (', 'FOREIGN KEY (' )||b.COLUMN_NAME||')' as consrc,
+ $sql =
+<<<EOD
+ select b.COLUMN_NAME as consrc,
+ a.CONSTRAINT_TYPE as contype
+ from ALL_CONSTRAINTS a, ALL_CONS_COLUMNS b
+ where (a.constraint_name = b.constraint_name AND a.table_name = b.table_name AND a.owner = b.owner)
+ and a.TABLE_NAME = '{$tableName}'
+ and a.OWNER = '{$schemaName}'
+ and a.CONSTRAINT_TYPE in ('P','R')
+EOD;
+ $this->getDbConnection()->setActive(true);
+ $command = $this->getDbConnection()->createCommand($sql);
+ //$command->bindValue(':table', $tableName);
+ //$command->bindValue(':schema', $schemaName);
+ $primary = array();
+ $foreign = array();
+ foreach($command->query() as $row)
+ {
+ switch( strtolower( $row['contype'] ) )
+ {
+ case 'p':
+ $primary = array_merge( $primary, array(strtolower( $row['consrc'] )) );
+ /*
+ $arr = $this->getPrimaryKeys($row['consrc']);
+ $primary = array_merge( $primary, array(strtolower( $arr[0] )) );
+ */
+ break;
+ case 'r':
+ $foreign = array_merge( $foreign, array(strtolower( $row['consrc'] )) );
+ /*
+ // if(($fkey = $this->getForeignKeys($row['consrc']))!==null)
+ $fkey = $this->getForeignKeys( $row['consrc'] );
+ $foreign = array_merge( $foreign, array(strtolower( $fkey )) );
+ */
+ break;
+ }
+ }
+ return array($primary,$foreign);
+ }
+
+ /**
+ * Gets the primary key field names
+ * @param string Oracle primary key definition
+ * @return array primary key field names.
+ */
+ protected function getPrimaryKeys($src)
+ {
+ $matches = array();
+ if(preg_match('/PRIMARY\s+KEY\s+\(([^\)]+)\)/i', $src, $matches))
+ return preg_split('/,\s+/',$matches[1]);
+ return array();
+ }
+
+ /**
+ * Gets foreign relationship constraint keys and table name
+ * @param string Oracle foreign key definition
+ * @return array foreign relationship table name and keys, null otherwise
+ */
+ protected function getForeignKeys($src)
+ {
+ $matches = array();
+ $brackets = '\(([^\)]+)\)';
+ $find = "/FOREIGN\s+KEY\s+{$brackets}\s+REFERENCES\s+([^\(]+){$brackets}/i";
+ if(preg_match($find, $src, $matches))
+ {
+ $keys = preg_split('/,\s+/', $matches[1]);
+ $fkeys = array();
+ foreach(preg_split('/,\s+/', $matches[3]) as $i => $fkey)
+ $fkeys[$keys[$i]] = $fkey;
+ return array('table' => str_replace('"','',$matches[2]), 'keys' => $fkeys);
+ }
+ }
+
+ /**
+ * @param string column name.
+ * @param TOracleTableInfo table information.
+ * @return boolean true if column is a foreign key.
+ */
+ protected function isForeignKeyColumn($columnId, $tableInfo)
+ {
+ foreach($tableInfo->getForeignKeys() as $fk)
+ {
+ if( $fk==$columnId )
+ //if(in_array($columnId, array_keys($fk['keys'])))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns all table names in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
+ * If not empty, the returned table names will be prefixed with the schema name.
+ * @return array all table names in the database.
+ */
+ public function findTableNames($schema='')
+ {
+ if($schema==='')
+ {
+ $sql=<<<EOD
+SELECT table_name, '{$schema}' as table_schema FROM user_tables
+EOD;
+ $command=$this->getDbConnection()->createCommand($sql);
+ }
+ else
+ {
+ $sql=<<<EOD
+SELECT object_name as table_name, owner as table_schema FROM all_objects
+WHERE object_type = 'TABLE' AND owner=:schema
+EOD;
+ $command=$this->getDbConnection()->createCommand($sql);
+ $command->bindParam(':schema',$schema);
+ }
+
+ $rows=$command->queryAll();
+ $names=array();
+ foreach($rows as $row)
+ {
+ if($schema===$this->getDefaultSchema() || $schema==='')
+ $names[]=$row['TABLE_NAME'];
+ else
+ $names[]=$row['TABLE_SCHEMA'].'.'.$row['TABLE_NAME'];
+ }
+ return $names;
+ }
+} \ No newline at end of file
diff --git a/lib/prado/framework/Data/Common/Oracle/TOracleTableColumn.php b/lib/prado/framework/Data/Common/Oracle/TOracleTableColumn.php
new file mode 100644
index 0000000..31a5e6c
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Oracle/TOracleTableColumn.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * TOracleTableColumn class file.
+ *
+ * @author Marcos Nobre <marconobre[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.Common.Oracle
+ */
+
+/**
+ * Load common TDbTableCommon class.
+ */
+Prado::using('System.Data.Common.TDbTableColumn');
+
+/**
+ * Describes the column metadata of the schema for a PostgreSQL database table.
+ *
+ * @author Marcos Nobre <marconobre[at]gmail[dot]com>
+ * @package System.Data.Common.Oracle
+ * @since 3.1
+ */
+class TOracleTableColumn extends TDbTableColumn
+{
+ private static $types=array(
+ 'numeric' => array( 'numeric' )
+// 'integer' => array('bit', 'bit varying', 'real', 'serial', 'int', 'integer'),
+// 'boolean' => array('boolean'),
+// 'float' => array('bigint', 'bigserial', 'double precision', 'money', 'numeric')
+ );
+
+ /**
+ * Overrides parent implementation, returns PHP type from the db type.
+ * @return boolean derived PHP primitive type from the column db type.
+ */
+ public function getPHPType()
+ {
+ $dbtype = strtolower($this->getDbType());
+ foreach(self::$types as $type => $dbtypes)
+ {
+ if(in_array($dbtype, $dbtypes))
+ return $type;
+ }
+ return 'string';
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Oracle/TOracleTableInfo.php b/lib/prado/framework/Data/Common/Oracle/TOracleTableInfo.php
new file mode 100644
index 0000000..f226562
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Oracle/TOracleTableInfo.php
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ * TOracleTableInfo class file.
+ *
+ * @author Marcos Nobre <marconobre[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.Common
+ */
+
+/**
+ * TDbTableInfo class describes the meta data of a database table.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TOracleTableInfo extends TComponent
+{
+ private $_info=array();
+
+ private $_primaryKeys;
+ private $_foreignKeys;
+
+ private $_columns;
+
+ private $_lowercase;
+
+ /**
+ * Sets the database table meta data information.
+ * @param array table column information.
+ */
+ public function __construct($tableInfo=array(),$primary=array(),$foreign=array())
+ {
+ $this->_info=$tableInfo;
+ $this->_primaryKeys=$primary;
+ $this->_foreignKeys=$foreign;
+ $this->_columns=new TMap;
+ }
+
+ /**
+ * @param TDbConnection database connection.
+ * @return TDbCommandBuilder new command builder
+ */
+ public function createCommandBuilder($connection)
+ {
+ Prado::using('System.Data.Common.Oracle.TOracleCommandBuilder');
+ return new TOracleCommandBuilder($connection,$this);
+ }
+
+ /**
+ * @param string information array key name
+ * @param mixed default value if information array value is null
+ * @return mixed information array value.
+ */
+ public function getInfo($name,$default=null)
+ {
+ return isset($this->_info[$name]) ? $this->_info[$name] : $default;
+ }
+
+ /**
+ * @param string information array key name
+ * @param mixed new information array value.
+ */
+ protected function setInfo($name,$value)
+ {
+ $this->_info[$name]=$value;
+ }
+
+ /**
+ * @return string name of the table this column belongs to.
+ */
+ public function getTableName()
+ {
+ return $this->getInfo('TableName');
+ }
+
+ /**
+ * @return string full name of the table, database dependent.
+ */
+ public function getTableFullName()
+ {
+ return $this->_info['SchemaName'].'.'.$this->getTableName();
+ }
+
+ /**
+ * @return boolean whether the table is a view, default is false.
+ */
+ public function getIsView()
+ {
+ return $this->getInfo('IsView',false);
+ }
+
+ /**
+ * @return TMap TDbTableColumn column meta data.
+ */
+ public function getColumns()
+ {
+ return $this->_columns;
+ }
+
+ /**
+ * @param string column id
+ * @return TDbTableColumn column information.
+ */
+ public function getColumn($name)
+ {
+ if(($column = $this->_columns->itemAt($name))!==null)
+ return $column;
+ throw new TDbException('dbtableinfo_invalid_column_name', $name, $this->getTableFullName());
+ }
+
+ /**
+ * @param array list of column Id, empty to get all columns.
+ * @return array table column names (identifier quoted)
+ */
+ public function getColumnNames()
+ {
+ foreach($this->getColumns() as $column)
+ $names[] = $column->getColumnName();
+ return $names;
+ }
+
+ /**
+ * @return string[] names of primary key columns.
+ */
+ public function getPrimaryKeys()
+ {
+ return $this->_primaryKeys;
+ }
+
+ /**
+ * @return array tuples of foreign table and column name.
+ */
+ public function getForeignKeys()
+ {
+ return $this->_foreignKeys;
+ }
+
+ /**
+ * @return array lowercased column key names mapped to normal column ids.
+ */
+ public function getLowerCaseColumnNames()
+ {
+ if($this->_lowercase===null)
+ {
+ $this->_lowercase=array();
+ foreach($this->getColumns()->getKeys() as $key)
+ $this->_lowercase[strtolower($key)] = $key;
+ }
+ return $this->_lowercase;
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Pgsql/TPgsqlCommandBuilder.php b/lib/prado/framework/Data/Common/Pgsql/TPgsqlCommandBuilder.php
new file mode 100644
index 0000000..5e219ad
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Pgsql/TPgsqlCommandBuilder.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * TPgsqlCommandBuilder 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.Common
+ */
+
+Prado::using('System.Data.Common.TDbCommandBuilder');
+
+/**
+ * TPgsqlCommandBuilder provides specifics methods to create limit/offset query commands
+ * for Pgsql database.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TPgsqlCommandBuilder extends TDbCommandBuilder
+{
+ /**
+ * Overrides parent implementation. Only column of type text or character (and its variants)
+ * accepts the LIKE criteria.
+ * @param array list of column id for potential search condition.
+ * @param string string of keywords
+ * @return string SQL search condition matching on a set of columns.
+ */
+ public function getSearchExpression($fields, $keywords)
+ {
+ $columns = array();
+ foreach($fields as $field)
+ {
+ if($this->isSearchableColumn($this->getTableInfo()->getColumn($field)))
+ $columns[] = $field;
+ }
+ return parent::getSearchExpression($columns, $keywords);
+ }
+ /**
+ *
+ * @return boolean true if column can be used for LIKE searching.
+ */
+ protected function isSearchableColumn($column)
+ {
+ $type = strtolower($column->getDbType());
+ return $type === 'character varying' || $type === 'varchar' ||
+ $type === 'character' || $type === 'char' || $type === 'text';
+ }
+
+ /**
+ * Overrides parent implementation to use PostgreSQL's ILIKE instead of LIKE (case-sensitive).
+ * @param string column name.
+ * @param array keywords
+ * @return string search condition for all words in one column.
+ */
+ protected function getSearchCondition($column, $words)
+ {
+ $conditions=array();
+ foreach($words as $word)
+ $conditions[] = $column.' ILIKE '.$this->getDbConnection()->quoteString('%'.$word.'%');
+ return '('.implode(' AND ', $conditions).')';
+ }
+
+}
+
diff --git a/lib/prado/framework/Data/Common/Pgsql/TPgsqlMetaData.php b/lib/prado/framework/Data/Common/Pgsql/TPgsqlMetaData.php
new file mode 100644
index 0000000..3001cf4
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Pgsql/TPgsqlMetaData.php
@@ -0,0 +1,448 @@
+<?php
+/**
+ * TPgsqlMetaData 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.Common.Pgsql
+ */
+
+/**
+ * Load the base TDbMetaData class.
+ */
+Prado::using('System.Data.Common.TDbMetaData');
+Prado::using('System.Data.Common.Pgsql.TPgsqlTableInfo');
+
+/**
+ * TPgsqlMetaData loads PostgreSQL database table and column information.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Pgsql
+ * @since 3.1
+ */
+class TPgsqlMetaData extends TDbMetaData
+{
+ private $_defaultSchema = 'public';
+
+ /**
+ * @return string TDbTableInfo class name.
+ */
+ protected function getTableInfoClass()
+ {
+ return 'TPgsqlTableInfo';
+ }
+
+ /**
+ * Quotes a table name for use in a query.
+ * @param string $name table name
+ * @return string the properly quoted table name
+ */
+ public function quoteTableName($name)
+ {
+ return parent::quoteTableName($name, '"', '"');
+ }
+
+ /**
+ * Quotes a column name for use in a query.
+ * @param string $name column name
+ * @return string the properly quoted column name
+ */
+ public function quoteColumnName($name)
+ {
+ return parent::quoteColumnName($name, '"', '"');
+ }
+
+ /**
+ * Quotes a column alias for use in a query.
+ * @param string $name column alias
+ * @return string the properly quoted column alias
+ */
+ public function quoteColumnAlias($name)
+ {
+ return parent::quoteColumnAlias($name, '"', '"');
+ }
+
+ /**
+ * @param string default schema.
+ */
+ public function setDefaultSchema($schema)
+ {
+ $this->_defaultSchema=$schema;
+ }
+
+ /**
+ * @return string default schema.
+ */
+ public function getDefaultSchema()
+ {
+ return $this->_defaultSchema;
+ }
+
+ /**
+ * @param string table name with optional schema name prefix, uses default schema name prefix is not provided.
+ * @return array tuple as ($schemaName,$tableName)
+ */
+ protected function getSchemaTableName($table)
+ {
+ if(count($parts= explode('.', str_replace('"','',$table))) > 1)
+ return array($parts[0], $parts[1]);
+ else
+ return array($this->getDefaultSchema(),$parts[0]);
+ }
+
+ /**
+ * Get the column definitions for given table.
+ * @param string table name.
+ * @return TPgsqlTableInfo table information.
+ */
+ protected function createTableInfo($table)
+ {
+ list($schemaName,$tableName) = $this->getSchemaTableName($table);
+
+ // This query is made much more complex by the addition of the 'attisserial' field.
+ // The subquery to get that field checks to see if there is an internally dependent
+ // sequence on the field.
+ $sql =
+<<<EOD
+ SELECT
+ a.attname,
+ pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
+ a.atttypmod,
+ a.attnotnull, a.atthasdef, adef.adsrc,
+ (
+ SELECT 1 FROM pg_catalog.pg_depend pd, pg_catalog.pg_class pc
+ WHERE pd.objid=pc.oid
+ AND pd.classid=pc.tableoid
+ AND pd.refclassid=pc.tableoid
+ AND pd.refobjid=a.attrelid
+ AND pd.refobjsubid=a.attnum
+ AND pd.deptype='i'
+ AND pc.relkind='S'
+ ) IS NOT NULL AS attisserial
+
+ FROM
+ pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef adef
+ ON a.attrelid=adef.adrelid
+ AND a.attnum=adef.adnum
+ LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid
+ WHERE
+ a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table
+ AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE
+ nspname = :schema))
+ AND a.attnum > 0 AND NOT a.attisdropped
+ ORDER BY a.attnum
+EOD;
+ $this->getDbConnection()->setActive(true);
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $tableName);
+ $command->bindValue(':schema', $schemaName);
+ $tableInfo = $this->createNewTableInfo($schemaName, $tableName);
+ $index=0;
+ foreach($command->query() as $col)
+ {
+ $col['index'] = $index++;
+ $this->processColumn($tableInfo, $col);
+ }
+ if($index===0)
+ throw new TDbException('dbmetadata_invalid_table_view', $table);
+ return $tableInfo;
+ }
+
+ /**
+ * @param string table schema name
+ * @param string table name.
+ * @return TPgsqlTableInfo
+ */
+ protected function createNewTableInfo($schemaName,$tableName)
+ {
+ $info['SchemaName'] = $this->assertIdentifier($schemaName);
+ $info['TableName'] = $this->assertIdentifier($tableName);
+ if($this->getIsView($schemaName,$tableName))
+ $info['IsView'] = true;
+ list($primary, $foreign) = $this->getConstraintKeys($schemaName, $tableName);
+ $class = $this->getTableInfoClass();
+ return new $class($info,$primary,$foreign);
+ }
+
+ /**
+ * @param string table name, schema name or column name.
+ * @return string a valid identifier.
+ * @throws TDbException when table name contains a double quote (").
+ */
+ protected function assertIdentifier($name)
+ {
+ if(strpos($name, '"')!==false)
+ {
+ $ref = 'http://www.postgresql.org/docs/7.4/static/sql-syntax.html#SQL-SYNTAX-IDENTIFIERS';
+ throw new TDbException('dbcommon_invalid_identifier_name', $name, $ref);
+ }
+ return $name;
+ }
+
+ /**
+ * @param string table schema name
+ * @param string table name.
+ * @return boolean true if the table is a view.
+ */
+ protected function getIsView($schemaName,$tableName)
+ {
+ $sql =
+<<<EOD
+ SELECT count(c.relname) FROM pg_catalog.pg_class c
+ LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
+ WHERE (n.nspname=:schema) AND (c.relkind = 'v'::"char") AND c.relname = :table
+EOD;
+ $this->getDbConnection()->setActive(true);
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':schema',$schemaName);
+ $command->bindValue(':table', $tableName);
+ return intval($command->queryScalar()) === 1;
+ }
+
+ /**
+ * @param TPgsqlTableInfo table information.
+ * @param array column information.
+ */
+ protected function processColumn($tableInfo, $col)
+ {
+ $columnId = $col['attname']; //use column name as column Id
+
+ $info['ColumnName'] = '"'.$columnId.'"'; //quote the column names!
+ $info['ColumnId'] = $columnId;
+ $info['ColumnIndex'] = $col['index'];
+ if(!$col['attnotnull'])
+ $info['AllowNull'] = true;
+ if(in_array($columnId, $tableInfo->getPrimaryKeys()))
+ $info['IsPrimaryKey'] = true;
+ if($this->isForeignKeyColumn($columnId, $tableInfo))
+ $info['IsForeignKey'] = true;
+
+ if($col['atttypmod'] > 0)
+ $info['ColumnSize'] = $col['atttypmod'] - 4;
+ if($col['atthasdef'])
+ $info['DefaultValue'] = $col['adsrc'];
+ if($col['attisserial'] || substr($col['adsrc'],0,8) === 'nextval(')
+ {
+ if(($sequence = $this->getSequenceName($tableInfo, $col['adsrc']))!==null)
+ {
+ $info['SequenceName'] = $sequence;
+ unset($info['DefaultValue']);
+ }
+ }
+ $matches = array();
+ if(preg_match('/\((\d+)(?:,(\d+))?+\)/', $col['type'], $matches))
+ {
+ $info['DbType'] = preg_replace('/\(\d+(?:,\d+)?\)/','',$col['type']);
+ if($this->isPrecisionType($info['DbType']))
+ {
+ $info['NumericPrecision'] = intval($matches[1]);
+ if(count($matches) > 2)
+ $info['NumericScale'] = intval($matches[2]);
+ }
+ else
+ $info['ColumnSize'] = intval($matches[1]);
+ }
+ else
+ $info['DbType'] = $col['type'];
+
+ $tableInfo->Columns[$columnId] = new TPgsqlTableColumn($info);
+ }
+
+ /**
+ * @return string serial name if found, null otherwise.
+ */
+ protected function getSequenceName($tableInfo,$src)
+ {
+ $matches = array();
+ if(preg_match('/nextval\([^\']*\'([^\']+)\'[^\)]*\)/i',$src,$matches))
+ {
+ if(is_int(strpos($matches[1], '.')))
+ return $matches[1];
+ else
+ return $tableInfo->getSchemaName().'.'.$matches[1];
+ }
+ }
+
+ /**
+ * @return boolean true if column type if "numeric", "interval" or begins with "time".
+ */
+ protected function isPrecisionType($type)
+ {
+ $type = strtolower(trim($type));
+ return $type==='numeric' || $type==='interval' || strpos($type, 'time')===0;
+ }
+
+ /**
+ * Gets the primary and foreign key column details for the given table.
+ * @param string schema name
+ * @param string table name.
+ * @return array tuple ($primary, $foreign)
+ */
+ protected function getConstraintKeys($schemaName, $tableName)
+ {
+ $sql =
+<<<EOD
+ SELECT conname, consrc, contype, indkey, indisclustered FROM (
+ SELECT
+ conname,
+ CASE WHEN contype='f' THEN
+ pg_catalog.pg_get_constraintdef(oid)
+ ELSE
+ 'CHECK (' || consrc || ')'
+ END AS consrc,
+ contype,
+ conrelid AS relid,
+ NULL AS indkey,
+ FALSE AS indisclustered
+ FROM
+ pg_catalog.pg_constraint
+ WHERE
+ contype IN ('f', 'c')
+ UNION ALL
+ SELECT
+ pc.relname,
+ NULL,
+ CASE WHEN indisprimary THEN
+ 'p'
+ ELSE
+ 'u'
+ END,
+ pi.indrelid,
+ indkey,
+ pi.indisclustered
+ FROM
+ pg_catalog.pg_class pc,
+ pg_catalog.pg_index pi
+ WHERE
+ pc.oid=pi.indexrelid
+ AND EXISTS (
+ SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
+ ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
+ WHERE d.classid = pc.tableoid AND d.objid = pc.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p')
+ )
+ ) AS sub
+ WHERE relid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table
+ AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace
+ WHERE nspname=:schema))
+ ORDER BY
+ 1
+EOD;
+ $this->getDbConnection()->setActive(true);
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $tableName);
+ $command->bindValue(':schema', $schemaName);
+ $primary = array();
+ $foreign = array();
+ foreach($command->query() as $row)
+ {
+ switch($row['contype'])
+ {
+ case 'p':
+ $primary = $this->getPrimaryKeys($tableName, $schemaName, $row['indkey']);
+ break;
+ case 'f':
+ if(($fkey = $this->getForeignKeys($row['consrc']))!==null)
+ $foreign[] = $fkey;
+ break;
+ }
+ }
+ return array($primary,$foreign);
+ }
+
+ /**
+ * Gets the primary key field names
+ * @param string pgsql primary key definition
+ * @return array primary key field names.
+ */
+ protected function getPrimaryKeys($tableName, $schemaName, $columnIndex)
+ {
+ $index = join(', ', explode(' ', $columnIndex));
+ $sql =
+<<<EOD
+ SELECT attnum, attname FROM pg_catalog.pg_attribute WHERE
+ attrelid=(
+ SELECT oid FROM pg_catalog.pg_class WHERE relname=:table AND relnamespace=(
+ SELECT oid FROM pg_catalog.pg_namespace WHERE nspname=:schema
+ )
+ )
+ AND attnum IN ({$index})
+EOD;
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $tableName);
+ $command->bindValue(':schema', $schemaName);
+// $command->bindValue(':columnIndex', join(', ', explode(' ', $columnIndex)));
+ $primary = array();
+ foreach($command->query() as $row)
+ {
+ $primary[] = $row['attname'];
+ }
+
+ return $primary;
+ }
+
+ /**
+ * Gets foreign relationship constraint keys and table name
+ * @param string pgsql foreign key definition
+ * @return array foreign relationship table name and keys, null otherwise
+ */
+ protected function getForeignKeys($src)
+ {
+ $matches = array();
+ $brackets = '\(([^\)]+)\)';
+ $find = "/FOREIGN\s+KEY\s+{$brackets}\s+REFERENCES\s+([^\(]+){$brackets}/i";
+ if(preg_match($find, $src, $matches))
+ {
+ $keys = preg_split('/,\s+/', $matches[1]);
+ $fkeys = array();
+ foreach(preg_split('/,\s+/', $matches[3]) as $i => $fkey)
+ $fkeys[$keys[$i]] = $fkey;
+ return array('table' => str_replace('"','',$matches[2]), 'keys' => $fkeys);
+ }
+ }
+
+ /**
+ * @param string column name.
+ * @param TPgsqlTableInfo table information.
+ * @return boolean true if column is a foreign key.
+ */
+ protected function isForeignKeyColumn($columnId, $tableInfo)
+ {
+ foreach($tableInfo->getForeignKeys() as $fk)
+ {
+ if(in_array($columnId, array_keys($fk['keys'])))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns all table names in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
+ * If not empty, the returned table names will be prefixed with the schema name.
+ * @return array all table names in the database.
+ */
+ public function findTableNames($schema='public')
+ {
+ if($schema==='')
+ $schema=self::DEFAULT_SCHEMA;
+ $sql=<<<EOD
+SELECT table_name, table_schema FROM information_schema.tables
+WHERE table_schema=:schema AND table_type='BASE TABLE'
+EOD;
+ $command=$this->getDbConnection()->createCommand($sql);
+ $command->bindParam(':schema',$schema);
+ $rows=$command->queryAll();
+ $names=array();
+ foreach($rows as $row)
+ {
+ if($schema===self::DEFAULT_SCHEMA)
+ $names[]=$row['table_name'];
+ else
+ $names[]=$row['table_schema'].'.'.$row['table_name'];
+ }
+ return $names;
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Pgsql/TPgsqlTableColumn.php b/lib/prado/framework/Data/Common/Pgsql/TPgsqlTableColumn.php
new file mode 100644
index 0000000..efe516e
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Pgsql/TPgsqlTableColumn.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * TPgsqlTableColumn 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.Common.Pgsql
+ */
+
+/**
+ * Load common TDbTableCommon class.
+ */
+Prado::using('System.Data.Common.TDbTableColumn');
+
+/**
+ * Describes the column metadata of the schema for a PostgreSQL database table.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Pgsql
+ * @since 3.1
+ */
+class TPgsqlTableColumn extends TDbTableColumn
+{
+ private static $types=array(
+ 'integer' => array('bit', 'bit varying', 'real', 'serial', 'int', 'integer'),
+ 'boolean' => array('boolean'),
+ 'float' => array('bigint', 'bigserial', 'double precision', 'money', 'numeric')
+ );
+
+ /**
+ * Overrides parent implementation, returns PHP type from the db type.
+ * @return boolean derived PHP primitive type from the column db type.
+ */
+ public function getPHPType()
+ {
+ $dbtype = strtolower($this->getDbType());
+ foreach(self::$types as $type => $dbtypes)
+ {
+ if(in_array($dbtype, $dbtypes))
+ return $type;
+ }
+ return 'string';
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Pgsql/TPgsqlTableInfo.php b/lib/prado/framework/Data/Common/Pgsql/TPgsqlTableInfo.php
new file mode 100644
index 0000000..aef09fa
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Pgsql/TPgsqlTableInfo.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * TPgsqlTableInfo 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.Common.Pgsql
+ */
+
+/**
+ * Loads the base TDbTableInfo class and TPgsqlTableColumn class.
+ */
+Prado::using('System.Data.Common.TDbTableInfo');
+Prado::using('System.Data.Common.Pgsql.TPgsqlTableColumn');
+
+/**
+ * TPgsqlTableInfo class provides additional table information for PostgreSQL database.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Pgsql
+ * @since 3.1
+ */
+class TPgsqlTableInfo extends TDbTableInfo
+{
+ /**
+ * @return string name of the schema this column belongs to.
+ */
+ public function getSchemaName()
+ {
+ return $this->getInfo('SchemaName');
+ }
+
+ /**
+ * @return string full name of the table, database dependent.
+ */
+ public function getTableFullName()
+ {
+ if(($schema=$this->getSchemaName())!==null)
+ return $schema.'.'.$this->getTableName();
+ else
+ return $this->getTableName();
+ }
+
+ /**
+ * @param TDbConnection database connection.
+ * @return TDbCommandBuilder new command builder
+ */
+ public function createCommandBuilder($connection)
+ {
+ Prado::using('System.Data.Common.Pgsql.TPgsqlCommandBuilder');
+ return new TPgsqlCommandBuilder($connection,$this);
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Sqlite/TSqliteCommandBuilder.php b/lib/prado/framework/Data/Common/Sqlite/TSqliteCommandBuilder.php
new file mode 100644
index 0000000..970ede4
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Sqlite/TSqliteCommandBuilder.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * TSqliteCommandBuilder 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.Common
+ */
+
+Prado::using('System.Data.Common.TDbCommandBuilder');
+
+/**
+ * TSqliteCommandBuilder provides specifics methods to create limit/offset query commands
+ * for Sqlite database.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TSqliteCommandBuilder extends TDbCommandBuilder
+{
+ /**
+ * Alters the sql to apply $limit and $offset.
+ * @param string SQL query string.
+ * @param integer maximum number of rows, -1 to ignore limit.
+ * @param integer row offset, -1 to ignore offset.
+ * @return string SQL with limit and offset.
+ */
+ public function applyLimitOffset($sql, $limit=-1, $offset=-1)
+ {
+ $limit = $limit!==null ? intval($limit) : -1;
+ $offset = $offset!==null ? intval($offset) : -1;
+ if($limit > 0 || $offset > 0)
+ {
+ $limitStr = ' LIMIT '.$limit;
+ $offsetStr = $offset >= 0 ? ' OFFSET '.$offset : '';
+ return $sql.$limitStr.$offsetStr;
+ }
+ else
+ return $sql;
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Sqlite/TSqliteMetaData.php b/lib/prado/framework/Data/Common/Sqlite/TSqliteMetaData.php
new file mode 100644
index 0000000..a2fe870
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Sqlite/TSqliteMetaData.php
@@ -0,0 +1,202 @@
+<?php
+/**
+ * TSqliteMetaData 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.Common.Sqlite
+ */
+
+/**
+ * Load the base TDbMetaData class.
+ */
+Prado::using('System.Data.Common.TDbMetaData');
+Prado::using('System.Data.Common.Sqlite.TSqliteTableInfo');
+
+/**
+ * TSqliteMetaData loads SQLite database table and column information.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Commom.Sqlite
+ * @since 3.1
+ */
+class TSqliteMetaData extends TDbMetaData
+{
+ /**
+ * @return string TDbTableInfo class name.
+ */
+ protected function getTableInfoClass()
+ {
+ return 'TSqliteTableInfo';
+ }
+
+ /**
+ * Quotes a table name for use in a query.
+ * @param string $name table name
+ * @return string the properly quoted table name
+ */
+ public function quoteTableName($name)
+ {
+ return parent::quoteTableName($name, "'", "'");
+ }
+
+ /**
+ * Quotes a column name for use in a query.
+ * @param string $name column name
+ * @return string the properly quoted column name
+ */
+ public function quoteColumnName($name)
+ {
+ return parent::quoteColumnName($name, '"', '"');
+ }
+
+ /**
+ * Quotes a column alias for use in a query.
+ * @param string $name column alias
+ * @return string the properly quoted column alias
+ */
+ public function quoteColumnAlias($name)
+ {
+ return parent::quoteColumnAlias($name, '"', '"');
+ }
+
+ /**
+ * Get the column definitions for given table.
+ * @param string table name.
+ * @return TPgsqlTableInfo table information.
+ */
+ protected function createTableInfo($tableName)
+ {
+ $tableName = str_replace("'",'',$tableName);
+ $this->getDbConnection()->setActive(true);
+ $table = $this->getDbConnection()->quoteString($tableName);
+ $sql = "PRAGMA table_info({$table})";
+ $command = $this->getDbConnection()->createCommand($sql);
+ $foreign = $this->getForeignKeys($table);
+ $index=0;
+ $columns=array();
+ $primary=array();
+ foreach($command->query() as $col)
+ {
+ $col['index'] = $index++;
+ $column = $this->processColumn($col, $foreign);
+ $columns[$col['name']] = $column;
+ if($column->getIsPrimaryKey())
+ $primary[] = $col['name'];
+ }
+ $info['TableName'] = $tableName;
+ if($this->getIsView($tableName))
+ $info['IsView'] = true;
+ if(count($columns)===0)
+ throw new TDbException('dbmetadata_invalid_table_view', $tableName);
+ $class = $this->getTableInfoClass();
+ $tableInfo = new $class($info,$primary,$foreign);
+ $tableInfo->getColumns()->copyFrom($columns);
+ return $tableInfo;
+ }
+
+ /**
+ * @param string table name.
+ * @return boolean true if the table is a view.
+ */
+ protected function getIsView($tableName)
+ {
+ $sql = 'SELECT count(*) FROM sqlite_master WHERE type="view" AND name= :table';
+ $this->getDbConnection()->setActive(true);
+ $command = $this->getDbConnection()->createCommand($sql);
+ $command->bindValue(':table', $tableName);
+ return intval($command->queryScalar()) === 1;
+ }
+
+ /**
+ * @param array column information.
+ * @param array foreign key details.
+ * @return TSqliteTableColumn column details.
+ */
+ protected function processColumn($col, $foreign)
+ {
+ $columnId = $col['name']; //use column name as column Id
+
+ $info['ColumnName'] = '"'.$columnId.'"'; //quote the column names!
+ $info['ColumnId'] = $columnId;
+ $info['ColumnIndex'] = $col['index'];
+
+ if($col['notnull']!=='99')
+ $info['AllowNull'] = true;
+
+ if($col['pk']==='1')
+ $info['IsPrimaryKey'] = true;
+ if($this->isForeignKeyColumn($columnId, $foreign))
+ $info['IsForeignKey'] = true;
+
+ if($col['dflt_value']!==null)
+ $info['DefaultValue'] = $col['dflt_value'];
+
+ $type = strtolower($col['type']);
+ $info['AutoIncrement'] = $type==='integer' && $col['pk']==='1';
+
+ $info['DbType'] = $type;
+ $match=array();
+ if(is_int($pos=strpos($type, '(')) && preg_match('/\((.*)\)/', $type, $match))
+ {
+ $ps = explode(',', $match[1]);
+ if(count($ps)===2)
+ {
+ $info['NumericPrecision'] = intval($ps[0]);
+ $info['NumericScale'] = intval($ps[1]);
+ }
+ else
+ $info['ColumnSize']=intval($match[1]);
+ $info['DbType'] = substr($type,0,$pos);
+ }
+
+ return new TSqliteTableColumn($info);
+ }
+
+ /**
+ *
+ *
+ * @param string quoted table name.
+ * @return array foreign key details.
+ */
+ protected function getForeignKeys($table)
+ {
+ $sql = "PRAGMA foreign_key_list({$table})";
+ $command = $this->getDbConnection()->createCommand($sql);
+ $fkeys = array();
+ foreach($command->query() as $col)
+ {
+ $fkeys[$col['table']]['keys'][$col['from']] = $col['to'];
+ $fkeys[$col['table']]['table'] = $col['table'];
+ }
+ return count($fkeys) > 0 ? array_values($fkeys) : $fkeys;
+ }
+
+ /**
+ * @param string column name.
+ * @param array foreign key column names.
+ * @return boolean true if column is a foreign key.
+ */
+ protected function isForeignKeyColumn($columnId, $foreign)
+ {
+ foreach($foreign as $fk)
+ {
+ if(in_array($columnId, array_keys($fk['keys'])))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns all table names in the database.
+ * @param string $schema the schema of the tables. This is not used for sqlite database.
+ * @return array all table names in the database.
+ */
+ public function findTableNames($schema='')
+ {
+ $sql="SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence'";
+ return $this->getDbConnection()->createCommand($sql)->queryColumn();
+ }
+} \ No newline at end of file
diff --git a/lib/prado/framework/Data/Common/Sqlite/TSqliteTableColumn.php b/lib/prado/framework/Data/Common/Sqlite/TSqliteTableColumn.php
new file mode 100644
index 0000000..e58ec85
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Sqlite/TSqliteTableColumn.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * TSqliteTableColumn 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.Common.Sqlite
+ */
+
+/**
+ * Load common TDbTableCommon class.
+ */
+Prado::using('System.Data.Common.TDbTableColumn');
+
+/**
+ * Describes the column metadata of the schema for a PostgreSQL database table.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Sqlite
+ * @since 3.1
+ */
+class TSqliteTableColumn extends TDbTableColumn
+{
+ /**
+ * @TODO add sqlite types.
+ */
+ private static $types = array();
+
+ /**
+ * Overrides parent implementation, returns PHP type from the db type.
+ * @return boolean derived PHP primitive type from the column db type.
+ */
+ public function getPHPType()
+ {
+ $dbtype = strtolower($this->getDbType());
+ foreach(self::$types as $type => $dbtypes)
+ {
+ if(in_array($dbtype, $dbtypes))
+ return $type;
+ }
+ return 'string';
+ }
+
+ /**
+ * @return boolean true if column will auto-increment when the column value is inserted as null.
+ */
+ public function getAutoIncrement()
+ {
+ return $this->getInfo('AutoIncrement', false);
+ }
+
+ /**
+ * @return boolean true if auto increment is true.
+ */
+ public function hasSequence()
+ {
+ return $this->getAutoIncrement();
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/Sqlite/TSqliteTableInfo.php b/lib/prado/framework/Data/Common/Sqlite/TSqliteTableInfo.php
new file mode 100644
index 0000000..6c047e4
--- /dev/null
+++ b/lib/prado/framework/Data/Common/Sqlite/TSqliteTableInfo.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * TSqliteTableInfo 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.Common.Sqlite
+ */
+
+/**
+ * Loads the base TDbTableInfo class and TSqliteTableColumn class.
+ */
+Prado::using('System.Data.Common.TDbTableInfo');
+Prado::using('System.Data.Common.Sqlite.TSqliteTableColumn');
+
+/**
+ * TSqliteTableInfo class provides additional table information for PostgreSQL database.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common.Sqlite
+ * @since 3.1
+ */
+class TSqliteTableInfo extends TDbTableInfo
+{
+ /**
+ * @param TDbConnection database connection.
+ * @return TDbCommandBuilder new command builder
+ */
+ public function createCommandBuilder($connection)
+ {
+ Prado::using('System.Data.Common.Sqlite.TSqliteCommandBuilder');
+ return new TSqliteCommandBuilder($connection,$this);
+ }
+
+ /**
+ * @return string full name of the table, database dependent.
+ */
+ public function getTableFullName()
+ {
+ return "'".$this->getTableName()."'";
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/TDbCommandBuilder.php b/lib/prado/framework/Data/Common/TDbCommandBuilder.php
new file mode 100644
index 0000000..e1d476e
--- /dev/null
+++ b/lib/prado/framework/Data/Common/TDbCommandBuilder.php
@@ -0,0 +1,505 @@
+<?php
+/**
+ * TDbCommandBuilder 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.Common
+ */
+
+/**
+ * TDbCommandBuilder provides basic methods to create query commands for tables
+ * giving by {@link setTableInfo TableInfo} the property.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TDbCommandBuilder extends TComponent
+{
+ private $_connection;
+ private $_tableInfo;
+
+ /**
+ * @param TDbConnection database connection.
+ * @param TDbTableInfo table information.
+ */
+ public function __construct($connection=null, $tableInfo=null)
+ {
+ $this->setDbConnection($connection);
+ $this->setTableInfo($tableInfo);
+ }
+
+ /**
+ * @return TDbConnection database connection.
+ */
+ public function getDbConnection()
+ {
+ return $this->_connection;
+ }
+
+ /**
+ * @param TDbConnection database connection.
+ */
+ public function setDbConnection($value)
+ {
+ $this->_connection=$value;
+ }
+
+ /**
+ * @param TDbTableInfo table information.
+ */
+ public function setTableInfo($value)
+ {
+ $this->_tableInfo=$value;
+ }
+
+ /**
+ * @param TDbTableInfo table information.
+ */
+ public function getTableInfo()
+ {
+ return $this->_tableInfo;
+ }
+
+ /**
+ * Iterate through all the columns and returns the last insert id of the
+ * first column that has a sequence or serial.
+ * @return mixed last insert id, null if none is found.
+ */
+ public function getLastInsertID()
+ {
+ foreach($this->getTableInfo()->getColumns() as $column)
+ {
+ if($column->hasSequence())
+ return $this->getDbConnection()->getLastInsertID($column->getSequenceName());
+ }
+ }
+
+ /**
+ * Alters the sql to apply $limit and $offset. Default implementation is applicable
+ * for PostgreSQL, MySQL and SQLite.
+ * @param string SQL query string.
+ * @param integer maximum number of rows, -1 to ignore limit.
+ * @param integer row offset, -1 to ignore offset.
+ * @return string SQL with limit and offset.
+ */
+ public function applyLimitOffset($sql, $limit=-1, $offset=-1)
+ {
+ $limit = $limit!==null ? (int)$limit : -1;
+ $offset = $offset!==null ? (int)$offset : -1;
+ $limitStr = $limit >= 0 ? ' LIMIT '.$limit : '';
+ $offsetStr = $offset >= 0 ? ' OFFSET '.$offset : '';
+ return $sql.$limitStr.$offsetStr;
+ }
+
+ /**
+ * @param string SQL string without existing ordering.
+ * @param array pairs of column names as key and direction as value.
+ * @return string modified SQL applied with ORDER BY.
+ */
+ public function applyOrdering($sql, $ordering)
+ {
+ $orders=array();
+ foreach($ordering as $name => $direction)
+ {
+ $direction = strtolower($direction) == 'desc' ? 'DESC' : 'ASC';
+ if(false !== strpos($name, '(') && false !== strpos($name, ')')) {
+ // key is a function (bad practice, but we need to handle it)
+ $key = $name;
+ } else {
+ // key is a column
+ $key = $this->getTableInfo()->getColumn($name)->getColumnName();
+ }
+ $orders[] = $key.' '.$direction;
+ }
+ if(count($orders) > 0)
+ $sql .= ' ORDER BY '.implode(', ', $orders);
+ return $sql;
+ }
+
+ /**
+ * Computes the SQL condition for search a set of column using regular expression
+ * (or LIKE, depending on database implementation) to match a string of
+ * keywords (default matches all keywords).
+ * @param array list of column id for potential search condition.
+ * @param string string of keywords
+ * @return string SQL search condition matching on a set of columns.
+ */
+ public function getSearchExpression($fields, $keywords)
+ {
+ if(strlen(trim($keywords)) == 0) return '';
+ $words = preg_split('/\s/u', $keywords);
+ $conditions = array();
+ foreach($fields as $field)
+ {
+ $column = $this->getTableInfo()->getColumn($field)->getColumnName();
+ $conditions[] = $this->getSearchCondition($column, $words);
+ }
+ return '('.implode(' OR ', $conditions).')';
+ }
+
+ /**
+ * @param string column name.
+ * @param array keywords
+ * @return string search condition for all words in one column.
+ */
+ protected function getSearchCondition($column, $words)
+ {
+ $conditions=array();
+ foreach($words as $word)
+ $conditions[] = $column.' LIKE '.$this->getDbConnection()->quoteString('%'.$word.'%');
+ return '('.implode(' AND ', $conditions).')';
+ }
+
+ /**
+ *
+ * Different behavior depends on type of passed data
+ * string
+ * usage without modification
+ *
+ * null
+ * will be expanded to full list of quoted table column names (quoting depends on database)
+ *
+ * array
+ * - Column names will be quoted if used as key or value of array
+ * <code>
+ * array('col1', 'col2', 'col2')
+ * // SELECT `col1`, `col2`, `col3` FROM...
+ * </code>
+ *
+ * - Column aliasing
+ * <code>
+ * array('mycol1' => 'col1', 'mycol2' => 'COUNT(*)')
+ * // SELECT `col1` AS mycol1, COUNT(*) AS mycol2 FROM...
+ * </code>
+ *
+ * - NULL and scalar values (strings will be quoted depending on database)
+ * <code>
+ * array('col1' => 'my custom string', 'col2' => 1.0, 'col3' => 'NULL')
+ * // SELECT "my custom string" AS `col1`, 1.0 AS `col2`, NULL AS `col3` FROM...
+ * </code>
+ *
+ * - If the *-wildcard char is used as key or value, add the full list of quoted table column names
+ * <code>
+ * array('col1' => 'NULL', '*')
+ * // SELECT `col1`, `col2`, `col3`, NULL AS `col1` FROM...
+ * </code>
+ * @param mixed $value
+ * @return array of generated fields - use implode(', ', $selectfieldlist) to collapse field list for usage
+ * @since 3.1.7
+ * @todo add support for table aliasing
+ * @todo add support for quoting of column aliasing
+ */
+ public function getSelectFieldList($data='*') {
+ if(is_scalar($data)) {
+ $tmp = explode(',', $data);
+ $result = array();
+ foreach($tmp as $v)
+ $result[] = trim($v);
+ return $result;
+ }
+
+ $bHasWildcard = false;
+ $result = array();
+ if(is_array($data) || $data instanceof Traversable) {
+ $columns = $this->getTableInfo()->getColumns();
+ foreach($data as $key=>$value) {
+ if($key==='*' || $value==='*') {
+ $bHasWildcard = true;
+ continue;
+ }
+
+ if(strToUpper($key)==='NULL') {
+ $result[] = 'NULL';
+ continue;
+ }
+
+ if(strpos($key, '(')!==false && strpos($key, ')')!==false) {
+ $result[] = $key;
+ continue;
+ }
+
+ if(stripos($key, 'AS')!==false) {
+ $result[] = $key;
+ continue;
+ }
+
+ if(stripos($value, 'AS')!==false) {
+ $result[] = $value;
+ continue;
+ }
+
+ $v = isset($columns[$value]);
+ $k = isset($columns[$key]);
+ if(is_integer($key) && $v) {
+ $key = $value;
+ $k = $v;
+ }
+
+ if(strToUpper($value)==='NULL') {
+ if($k)
+ $result[] = 'NULL AS ' . $columns[$key]->getColumnName();
+ else
+ $result[] = 'NULL' . (is_string($key) ? (' AS ' . (string)$key) : '');
+ continue;
+ }
+
+ if(strpos($value, '(')!==false && strpos($value, ')')!==false) {
+ if($k)
+ $result[] = $value . ' AS ' . $columns[$key]->getColumnName();
+ else
+ $result[] = $value . (is_string($key) ? (' AS ' . (string)$key) : '');
+ continue;
+ }
+
+ if($v && $key==$value) {
+ $result[] = $columns[$value]->getColumnName();
+ continue;
+ }
+
+ if($k && $value==null) {
+ $result[] = $columns[$key]->getColumnName();
+ continue;
+ }
+
+ if(is_string($key) && $v) {
+ $result[] = $columns[$value]->getColumnName() . ' AS ' . $key;
+ continue;
+ }
+
+ if(is_numeric($value) && $k) {
+ $result[] = $value . ' AS ' . $columns[$key]->getColumnName();
+ continue;
+ }
+
+ if(is_string($value) && $k) {
+ $result[] = $this->getDbConnection()->quoteString($value) . ' AS ' . $columns[$key]->getColumnName();
+ continue;
+ }
+
+ if(!$v && !$k && is_integer($key)) {
+ $result[] = is_numeric($value) ? $value : $this->getDbConnection()->quoteString((string)$value);
+ continue;
+ }
+
+ $result[] = (is_numeric($value) ? $value : $this->getDbConnection()->quoteString((string)$value)) . ' AS ' . $key;
+ }
+ }
+
+ if($data===null || count($result) == 0 || $bHasWildcard)
+ $result = $result = array_merge($this->getTableInfo()->getColumnNames(), $result);
+
+ return $result;
+ }
+
+ /**
+ * Appends the $where condition to the string "SELECT * FROM tableName WHERE ".
+ * The tableName is obtained from the {@link setTableInfo TableInfo} property.
+ * @param string query condition
+ * @param array condition parameters.
+ * @return TDbCommand query command.
+ */
+ public function createFindCommand($where='1=1', $parameters=array(), $ordering=array(), $limit=-1, $offset=-1, $select='*')
+ {
+ $table = $this->getTableInfo()->getTableFullName();
+ $fields = implode(', ', $this -> getSelectFieldList($select));
+ $sql = "SELECT {$fields} FROM {$table}";
+ if(!empty($where))
+ $sql .= " WHERE {$where}";
+ return $this->applyCriterias($sql, $parameters, $ordering, $limit, $offset);
+ }
+
+ public function applyCriterias($sql, $parameters=array(),$ordering=array(), $limit=-1, $offset=-1)
+ {
+ if(count($ordering) > 0)
+ $sql = $this->applyOrdering($sql, $ordering);
+ if($limit>=0 || $offset>=0)
+ $sql = $this->applyLimitOffset($sql, $limit, $offset);
+ $command = $this->createCommand($sql);
+ $this->bindArrayValues($command, $parameters);
+ return $command;
+ }
+
+ /**
+ * Creates a count(*) command for the table described in {@link setTableInfo TableInfo}.
+ * @param string count condition.
+ * @param array binding parameters.
+ * @return TDbCommand count command.
+ */
+ public function createCountCommand($where='1=1', $parameters=array(),$ordering=array(), $limit=-1, $offset=-1)
+ {
+ return $this->createFindCommand($where, $parameters, $ordering, $limit, $offset, 'COUNT(*)');
+ }
+
+ /**
+ * Creates a delete command for the table described in {@link setTableInfo TableInfo}.
+ * The conditions for delete is given by the $where argument and the parameters
+ * for the condition is given by $parameters.
+ * @param string delete condition.
+ * @param array delete parameters.
+ * @return TDbCommand delete command.
+ */
+ public function createDeleteCommand($where,$parameters=array())
+ {
+ $table = $this->getTableInfo()->getTableFullName();
+ if (!empty($where))
+ $where = ' WHERE '.$where;
+ $command = $this->createCommand("DELETE FROM {$table}".$where);
+ $this->bindArrayValues($command, $parameters);
+ return $command;
+ }
+
+ /**
+ * Creates an insert command for the table described in {@link setTableInfo TableInfo} for the given data.
+ * Each array key in the $data array must correspond to the column name of the table
+ * (if a column allows to be null, it may be omitted) to be inserted with
+ * the corresponding array value.
+ * @param array name-value pairs of new data to be inserted.
+ * @return TDbCommand insert command
+ */
+ public function createInsertCommand($data)
+ {
+ $table = $this->getTableInfo()->getTableFullName();
+ list($fields, $bindings) = $this->getInsertFieldBindings($data);
+ $command = $this->createCommand("INSERT INTO {$table}({$fields}) VALUES ($bindings)");
+ $this->bindColumnValues($command, $data);
+ return $command;
+ }
+
+ /**
+ * Creates an update command for the table described in {@link setTableInfo TableInfo} for the given data.
+ * Each array key in the $data array must correspond to the column name to be updated with the corresponding array value.
+ * @param array name-value pairs of data to be updated.
+ * @param string update condition.
+ * @param array update parameters.
+ * @return TDbCommand update command.
+ */
+ public function createUpdateCommand($data, $where, $parameters=array())
+ {
+ $table = $this->getTableInfo()->getTableFullName();
+ if($this->hasIntegerKey($parameters))
+ $fields = implode(', ', $this->getColumnBindings($data, true));
+ else
+ $fields = implode(', ', $this->getColumnBindings($data));
+
+ if (!empty($where))
+ $where = ' WHERE '.$where;
+ $command = $this->createCommand("UPDATE {$table} SET {$fields}".$where);
+ $this->bindArrayValues($command, array_merge($data, $parameters));
+ return $command;
+ }
+
+ /**
+ * Returns a list of insert field name and a list of binding names.
+ * @param object array or object to be inserted.
+ * @return array tuple ($fields, $bindings)
+ */
+ protected function getInsertFieldBindings($values)
+ {
+ $fields = array(); $bindings=array();
+ foreach(array_keys($values) as $name)
+ {
+ $fields[] = $this->getTableInfo()->getColumn($name)->getColumnName();
+ $bindings[] = ':'.$name;
+ }
+ return array(implode(', ',$fields), implode(', ', $bindings));
+ }
+
+ /**
+ * Create a name-value or position-value if $position=true binding strings.
+ * @param array data for binding.
+ * @param boolean true to bind as position values.
+ * @return string update column names with corresponding binding substrings.
+ */
+ protected function getColumnBindings($values, $position=false)
+ {
+ $bindings=array();
+ foreach(array_keys($values) as $name)
+ {
+ $column = $this->getTableInfo()->getColumn($name)->getColumnName();
+ $bindings[] = $position ? $column.' = ?' : $column.' = :'.$name;
+ }
+ return $bindings;
+ }
+
+ /**
+ * @param string SQL query string.
+ * @return TDbCommand corresponding database command.
+ */
+ public function createCommand($sql)
+ {
+ $this->getDbConnection()->setActive(true);
+ return $this->getDbConnection()->createCommand($sql);
+ }
+
+ /**
+ * Bind the name-value pairs of $values where the array keys correspond to column names.
+ * @param TDbCommand database command.
+ * @param array name-value pairs.
+ */
+ public function bindColumnValues($command, $values)
+ {
+ foreach($values as $name=>$value)
+ {
+ $column = $this->getTableInfo()->getColumn($name);
+ if($value === null && $column->getAllowNull())
+ $command->bindValue(':'.$name, null, PDO::PARAM_NULL);
+ else
+ $command->bindValue(':'.$name, $value, $column->getPdoType());
+ }
+ }
+
+ /**
+ * @param TDbCommand database command
+ * @param array values for binding.
+ */
+ public function bindArrayValues($command, $values)
+ {
+ if($this->hasIntegerKey($values))
+ {
+ $values = array_values($values);
+ for($i = 0, $max=count($values); $i<$max; $i++)
+ $command->bindValue($i+1, $values[$i], $this->getPdoType($values[$i]));
+ }
+ else
+ {
+ foreach($values as $name=>$value)
+ {
+ $prop = $name[0]===':' ? $name : ':'.$name;
+ $command->bindValue($prop, $value, $this->getPdoType($value));
+ }
+ }
+ }
+
+ /**
+ * @param mixed PHP value
+ * @return integer PDO parameter types.
+ */
+ public static function getPdoType($value)
+ {
+ switch(gettype($value))
+ {
+ case 'boolean': return PDO::PARAM_BOOL;
+ case 'integer': return PDO::PARAM_INT;
+ case 'string' : return PDO::PARAM_STR;
+ case 'NULL' : return PDO::PARAM_NULL;
+ }
+ }
+
+ /**
+ * @param array
+ * @return boolean true if any array key is an integer.
+ */
+ protected function hasIntegerKey($array)
+ {
+ foreach($array as $k=>$v)
+ {
+ if(gettype($k)==='integer')
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/lib/prado/framework/Data/Common/TDbMetaData.php b/lib/prado/framework/Data/Common/TDbMetaData.php
new file mode 100644
index 0000000..0ec9ce4
--- /dev/null
+++ b/lib/prado/framework/Data/Common/TDbMetaData.php
@@ -0,0 +1,192 @@
+<?php
+/**
+ * TDbMetaData 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.Common
+ */
+
+/**
+ * TDbMetaData is the base class for retrieving metadata information, such as
+ * table and columns information, from a database connection.
+ *
+ * Use the {@link getTableInfo} method to retrieve a table information.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+abstract class TDbMetaData extends TComponent
+{
+ private $_tableInfoCache=array();
+ private $_connection;
+
+ /**
+ * @var array
+ */
+ protected static $delimiterIdentifier = array('[', ']', '"', '`', "'");
+
+ /**
+ * @param TDbConnection database connection.
+ */
+ public function __construct($conn)
+ {
+ $this->_connection=$conn;
+ }
+
+ /**
+ * @return TDbConnection database connection.
+ */
+ public function getDbConnection()
+ {
+ return $this->_connection;
+ }
+
+ /**
+ * Obtain database specific TDbMetaData class using the driver name of the database connection.
+ * @param TDbConnection database connection.
+ * @return TDbMetaData database specific TDbMetaData.
+ */
+ public static function getInstance($conn)
+ {
+ $conn->setActive(true); //must be connected before retrieving driver name
+ $driver = $conn->getDriverName();
+ switch(strtolower($driver))
+ {
+ case 'pgsql':
+ Prado::using('System.Data.Common.Pgsql.TPgsqlMetaData');
+ return new TPgsqlMetaData($conn);
+ case 'mysqli':
+ case 'mysql':
+ Prado::using('System.Data.Common.Mysql.TMysqlMetaData');
+ return new TMysqlMetaData($conn);
+ case 'sqlite': //sqlite 3
+ case 'sqlite2': //sqlite 2
+ Prado::using('System.Data.Common.Sqlite.TSqliteMetaData');
+ return new TSqliteMetaData($conn);
+ case 'mssql': // Mssql driver on windows hosts
+ case 'sqlsrv': // sqlsrv driver on windows hosts
+ case 'dblib': // dblib drivers on linux (and maybe others os) hosts
+ Prado::using('System.Data.Common.Mssql.TMssqlMetaData');
+ return new TMssqlMetaData($conn);
+ case 'oci':
+ Prado::using('System.Data.Common.Oracle.TOracleMetaData');
+ return new TOracleMetaData($conn);
+// case 'ibm':
+// Prado::using('System.Data.Common.IbmDb2.TIbmDb2MetaData');
+// return new TIbmDb2MetaData($conn);
+ default:
+ throw new TDbException('ar_invalid_database_driver',$driver);
+ }
+ }
+
+ /**
+ * Obtains table meta data information for the current connection and given table name.
+ * @param string table or view name
+ * @return TDbTableInfo table information.
+ */
+ public function getTableInfo($tableName=null)
+ {
+ $key = $tableName===null?$this->getDbConnection()->getConnectionString():$tableName;
+ if(!isset($this->_tableInfoCache[$key]))
+ {
+ $class = $this->getTableInfoClass();
+ $tableInfo = $tableName===null ? new $class : $this->createTableInfo($tableName);
+ $this->_tableInfoCache[$key] = $tableInfo;
+ }
+ return $this->_tableInfoCache[$key];
+ }
+
+ /**
+ * Creates a command builder for a given table name.
+ * @param string table name.
+ * @return TDbCommandBuilder command builder instance for the given table.
+ */
+ public function createCommandBuilder($tableName=null)
+ {
+ return $this->getTableInfo($tableName)->createCommandBuilder($this->getDbConnection());
+ }
+
+ /**
+ * This method should be implemented by decendent classes.
+ * @return TDbTableInfo driver dependent create builder.
+ */
+ abstract protected function createTableInfo($tableName);
+
+ /**
+ * @return string TDbTableInfo class name.
+ */
+ protected function getTableInfoClass()
+ {
+ return 'TDbTableInfo';
+ }
+
+ /**
+ * Quotes a table name for use in a query.
+ * @param string $name table name
+ * @param string $lft left delimiter
+ * @param string $rgt right delimiter
+ * @return string the properly quoted table name
+ */
+ public function quoteTableName($name)
+ {
+ $name = str_replace(self::$delimiterIdentifier, '', $name);
+
+ $args = func_get_args();
+ $rgt = $lft = isset($args[1]) ? $args[1] : '';
+ $rgt = isset($args[2]) ? $args[2] : $rgt;
+
+ if(strpos($name, '.')===false)
+ return $lft . $name . $rgt;
+ $names=explode('.', $name);
+ foreach($names as &$n)
+ $n = $lft . $n . $rgt;
+ return implode('.', $names);
+ }
+
+ /**
+ * Quotes a column name for use in a query.
+ * @param string $name column name
+ * @param string $lft left delimiter
+ * @param string $rgt right delimiter
+ * @return string the properly quoted column name
+ */
+ public function quoteColumnName($name)
+ {
+ $args = func_get_args();
+ $rgt = $lft = isset($args[1]) ? $args[1] : '';
+ $rgt = isset($args[2]) ? $args[2] : $rgt;
+
+ return $lft . str_replace(self::$delimiterIdentifier, '', $name) . $rgt;
+ }
+
+ /**
+ * Quotes a column alias for use in a query.
+ * @param string $name column alias
+ * @param string $lft left delimiter
+ * @param string $rgt right delimiter
+ * @return string the properly quoted column alias
+ */
+ public function quoteColumnAlias($name)
+ {
+ $args = func_get_args();
+ $rgt = $lft = isset($args[1]) ? $args[1] : '';
+ $rgt = isset($args[2]) ? $args[2] : $rgt;
+
+ return $lft . str_replace(self::$delimiterIdentifier, '', $name) . $rgt;
+ }
+
+ /**
+ * Returns all table names in the database.
+ * This method should be overridden by child classes in order to support this feature
+ * because the default implementation simply throws an exception.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
+ * If not empty, the returned table names will be prefixed with the schema name.
+ * @return array all table names in the database.
+ */
+ abstract public function findTableNames($schema='');
+}
+
diff --git a/lib/prado/framework/Data/Common/TDbTableColumn.php b/lib/prado/framework/Data/Common/TDbTableColumn.php
new file mode 100644
index 0000000..9987197
--- /dev/null
+++ b/lib/prado/framework/Data/Common/TDbTableColumn.php
@@ -0,0 +1,197 @@
+<?php
+/**
+ * TDbTableColumn 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.Common
+ */
+
+/**
+ * TDbTableColumn class describes the column meta data of the schema for a database table.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TDbTableColumn extends TComponent
+{
+ const UNDEFINED_VALUE= INF; //use infinity for undefined value
+
+ private $_info=array();
+
+ /**
+ * Sets the table column meta data.
+ * @param array table column information.
+ */
+ public function __construct($columnInfo)
+ {
+ $this->_info=$columnInfo;
+ }
+
+ /**
+ * @param string information array key name
+ * @param mixed default value if information array value is null
+ * @return mixed information array value.
+ */
+ protected function getInfo($name,$default=null)
+ {
+ return isset($this->_info[$name]) ? $this->_info[$name] : $default;
+ }
+
+ /**
+ * @param string information array key name
+ * @param mixed new information array value.
+ */
+ protected function setInfo($name,$value)
+ {
+ $this->_info[$name]=$value;
+ }
+
+ /**
+ * Returns the derived PHP primitive type from the db type. Default returns 'string'.
+ * @return string derived PHP primitive type from the column db type.
+ */
+ public function getPHPType()
+ {
+ return 'string';
+ }
+
+ /**
+ * @param integer PDO bind param/value types, default returns string.
+ */
+ public function getPdoType()
+ {
+ switch($this->getPHPType())
+ {
+ case 'boolean': return PDO::PARAM_BOOL;
+ case 'integer': return PDO::PARAM_INT;
+ case 'string' : return PDO::PARAM_STR;
+ }
+ return PDO::PARAM_STR;
+ }
+
+ /**
+ * @return string name of the column in the table (identifier quoted).
+ */
+ public function getColumnName()
+ {
+ return $this->getInfo('ColumnName');
+ }
+
+ /**
+ * @return string name of the column with quoted identifier.
+ */
+ public function getColumnId()
+ {
+ return $this->getInfo('ColumnId');
+ }
+
+ /**
+ * @return string size of the column.
+ */
+ public function getColumnSize()
+ {
+ return $this->getInfo('ColumnSize');
+ }
+
+ /**
+ * @return integer zero-based ordinal position of the column in the table.
+ */
+ public function getColumnIndex()
+ {
+ return $this->getInfo('ColumnIndex');
+ }
+
+ /**
+ * @return string column type.
+ */
+ public function getDbType()
+ {
+ return $this->getInfo('DbType');
+ }
+
+ /**
+ * @return boolean specifies whether value Null is allowed, default is false.
+ */
+ public function getAllowNull()
+ {
+ return $this->getInfo('AllowNull',false);
+ }
+
+ /**
+ * @return mixed default column value if column value was null.
+ */
+ public function getDefaultValue()
+ {
+ return $this->getInfo('DefaultValue', self::UNDEFINED_VALUE);
+ }
+
+ /**
+ * @return string precision of the column data, if the data is numeric.
+ */
+ public function getNumericPrecision()
+ {
+ return $this->getInfo('NumericPrecision');
+ }
+
+ /**
+ * @return string scale of the column data, if the data is numeric.
+ */
+ public function getNumericScale()
+ {
+ return $this->getInfo('NumericScale');
+ }
+
+ public function getMaxiumNumericConstraint()
+ {
+ if(($precision=$this->getNumericPrecision())!==null)
+ {
+ $scale=$this->getNumericScale();
+ return $scale===null ? pow(10,$precision) : pow(10,$precision-$scale);
+ }
+ }
+
+ /**
+ * @return boolean whether this column is a primary key for the table, default is false.
+ */
+ public function getIsPrimaryKey()
+ {
+ return $this->getInfo('IsPrimaryKey',false);
+ }
+
+ /**
+ * @return boolean whether this column is a foreign key, default is false.
+ */
+ public function getIsForeignKey()
+ {
+ return $this->getInfo('IsForeignKey',false);
+ }
+
+ /**
+ * @param string sequence name, only applicable if column is a sequence
+ */
+ public function getSequenceName()
+ {
+ return $this->getInfo('SequenceName');
+ }
+
+ /**
+ * @return boolean whether the column is a sequence.
+ */
+ public function hasSequence()
+ {
+ return $this->getSequenceName()!==null;
+ }
+
+ /**
+ * @return boolean whether this column is excluded from insert and update.
+ */
+ public function getIsExcluded()
+ {
+ return false;
+ }
+}
+
diff --git a/lib/prado/framework/Data/Common/TDbTableInfo.php b/lib/prado/framework/Data/Common/TDbTableInfo.php
new file mode 100644
index 0000000..8e60157
--- /dev/null
+++ b/lib/prado/framework/Data/Common/TDbTableInfo.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * TDbTableInfo 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.Common
+ */
+
+/**
+ * TDbTableInfo class describes the meta data of a database table.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.Common
+ * @since 3.1
+ */
+class TDbTableInfo extends TComponent
+{
+ private $_info=array();
+
+ private $_primaryKeys;
+ private $_foreignKeys;
+
+ private $_columns;
+
+ private $_lowercase;
+
+ /**
+ * @var null|array
+ * @since 3.1.7
+ */
+ private $_names = null;
+
+ /**
+ * Sets the database table meta data information.
+ * @param array table column information.
+ */
+ public function __construct($tableInfo=array(),$primary=array(),$foreign=array())
+ {
+ $this->_info=$tableInfo;
+ $this->_primaryKeys=$primary;
+ $this->_foreignKeys=$foreign;
+ $this->_columns=new TMap;
+ }
+
+ /**
+ * @param TDbConnection database connection.
+ * @return TDbCommandBuilder new command builder
+ */
+ public function createCommandBuilder($connection)
+ {
+ Prado::using('System.Data.Common.TDbCommandBuilder');
+ return new TDbCommandBuilder($connection,$this);
+ }
+
+ /**
+ * @param string information array key name
+ * @param mixed default value if information array value is null
+ * @return mixed information array value.
+ */
+ protected function getInfo($name,$default=null)
+ {
+ return isset($this->_info[$name]) ? $this->_info[$name] : $default;
+ }
+
+ /**
+ * @param string information array key name
+ * @param mixed new information array value.
+ */
+ protected function setInfo($name,$value)
+ {
+ $this->_info[$name]=$value;
+ }
+
+ /**
+ * @return string name of the table this column belongs to.
+ */
+ public function getTableName()
+ {
+ return $this->getInfo('TableName');
+ }
+
+ /**
+ * @return string full name of the table, database dependent.
+ */
+ public function getTableFullName()
+ {
+ return $this->getTableName();
+ }
+
+ /**
+ * @return boolean whether the table is a view, default is false.
+ */
+ public function getIsView()
+ {
+ return $this->getInfo('IsView',false);
+ }
+
+ /**
+ * @return TMap TDbTableColumn column meta data.
+ */
+ public function getColumns()
+ {
+ return $this->_columns;
+ }
+
+ /**
+ * @param string column id
+ * @return TDbTableColumn column information.
+ */
+ public function getColumn($name)
+ {
+ if(($column = $this->_columns->itemAt($name))!==null)
+ return $column;
+ throw new TDbException('dbtableinfo_invalid_column_name', $name, $this->getTableFullName());
+ }
+
+ /**
+ * @param array list of column Id, empty to get all columns.
+ * @return array table column names (identifier quoted)
+ */
+ public function getColumnNames()
+ {
+ if($this->_names===null)
+ {
+ $this->_names=array();
+ foreach($this->getColumns() as $column)
+ $this->_names[] = $column->getColumnName();
+ }
+ return $this->_names;
+ }
+
+ /**
+ * @return string[] names of primary key columns.
+ */
+ public function getPrimaryKeys()
+ {
+ return $this->_primaryKeys;
+ }
+
+ /**
+ * @return array tuples of foreign table and column name.
+ */
+ public function getForeignKeys()
+ {
+ return $this->_foreignKeys;
+ }
+
+ /**
+ * @return array lowercased column key names mapped to normal column ids.
+ */
+ public function getLowerCaseColumnNames()
+ {
+ if($this->_lowercase===null)
+ {
+ $this->_lowercase=array();
+ foreach($this->getColumns()->getKeys() as $key)
+ $this->_lowercase[strtolower($key)] = $key;
+ }
+ return $this->_lowercase;
+ }
+} \ No newline at end of file
diff --git a/lib/prado/framework/Data/DataGateway/TDataGatewayCommand.php b/lib/prado/framework/Data/DataGateway/TDataGatewayCommand.php
new file mode 100644
index 0000000..d314ca0
--- /dev/null
+++ b/lib/prado/framework/Data/DataGateway/TDataGatewayCommand.php
@@ -0,0 +1,544 @@
+<?php
+/**
+ * TDataGatewayCommand, TDataGatewayEventParameter and TDataGatewayResultEventParameter 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.DataGateway
+ */
+
+/**
+ * TDataGatewayCommand is command builder and executor class for
+ * TTableGateway and TActiveRecordGateway.
+ *
+ * TDataGatewayCommand builds the TDbCommand for TTableGateway
+ * and TActiveRecordGateway commands such as find(), update(), insert(), etc,
+ * using the TDbCommandBuilder classes (database specific TDbCommandBuilder
+ * classes are used).
+ *
+ * Once the command is built and the query parameters are binded, the
+ * {@link OnCreateCommand} event is raised. Event handlers for the OnCreateCommand
+ * event should not alter the Command property nor the Criteria property of the
+ * TDataGatewayEventParameter.
+ *
+ * TDataGatewayCommand excutes the TDbCommands and returns the result obtained from the
+ * database (returned value depends on the method executed). The
+ * {@link OnExecuteCommand} event is raised after the command is executed and resulting
+ * data is set in the TDataGatewayResultEventParameter object's Result property.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.DataGateway
+ * @since 3.1
+ */
+class TDataGatewayCommand extends TComponent
+{
+ private $_builder;
+
+ /**
+ * @param TDbCommandBuilder database specific database command builder.
+ */
+ public function __construct($builder)
+ {
+ $this->_builder = $builder;
+ }
+
+ /**
+ * @return TDbTableInfo
+ */
+ public function getTableInfo()
+ {
+ return $this->_builder->getTableInfo();
+ }
+
+ /**
+ * @return TDbConnection
+ */
+ public function getDbConnection()
+ {
+ return $this->_builder->getDbConnection();
+ }
+
+ /**
+ * @return TDbCommandBuilder
+ */
+ public function getBuilder()
+ {
+ return $this->_builder;
+ }
+
+ /**
+ * Executes a delete command.
+ * @param TSqlCriteria delete conditions and parameters.
+ * @return integer number of records affected.
+ */
+ public function delete($criteria)
+ {
+ $where = $criteria->getCondition();
+ $parameters = $criteria->getParameters()->toArray();
+ $command = $this->getBuilder()->createDeleteCommand($where, $parameters);
+ $this->onCreateCommand($command,$criteria);
+ $command->prepare();
+ return $command->execute();
+ }
+
+ /**
+ * Updates the table with new data.
+ * @param array date for update.
+ * @param TSqlCriteria update conditions and parameters.
+ * @return integer number of records affected.
+ */
+ public function update($data, $criteria)
+ {
+ $where = $criteria->getCondition();
+ $parameters = $criteria->getParameters()->toArray();
+ $command = $this->getBuilder()->createUpdateCommand($data,$where, $parameters);
+ $this->onCreateCommand($command,$criteria);
+ $command->prepare();
+ return $this->onExecuteCommand($command, $command->execute());
+ }
+
+ /**
+ * @param array update for update
+ * @param array primary key-value name pairs.
+ * @return integer number of records affected.
+ */
+ public function updateByPk($data, $keys)
+ {
+ list($where, $parameters) = $this->getPrimaryKeyCondition((array)$keys);
+ return $this->update($data, new TSqlCriteria($where, $parameters));
+ }
+
+ /**
+ * Find one record matching the critera.
+ * @param TSqlCriteria find conditions and parameters.
+ * @return array matching record.
+ */
+ public function find($criteria)
+ {
+ $command = $this->getFindCommand($criteria);
+ return $this->onExecuteCommand($command, $command->queryRow());
+ }
+
+ /**
+ * Find one or more matching records.
+ * @param TSqlCriteria $criteria
+ * @return TDbDataReader record reader.
+ */
+ public function findAll($criteria)
+ {
+ $command = $this->getFindCommand($criteria);
+ return $this->onExecuteCommand($command, $command->query());
+ }
+
+ /**
+ * Build the find command from the criteria. Limit, Offset and Ordering are applied if applicable.
+ * @param TSqlCriteria $criteria
+ * @return TDbCommand.
+ */
+ protected function getFindCommand($criteria)
+ {
+ if($criteria===null)
+ return $this->getBuilder()->createFindCommand();
+ $where = $criteria->getCondition();
+ $parameters = $criteria->getParameters()->toArray();
+ $ordering = $criteria->getOrdersBy();
+ $limit = $criteria->getLimit();
+ $offset = $criteria->getOffset();
+ $select = $criteria->getSelect();
+ $command = $this->getBuilder()->createFindCommand($where,$parameters,$ordering,$limit,$offset,$select);
+ $this->onCreateCommand($command, $criteria);
+ return $command;
+ }
+
+ /**
+ * @param mixed primary key value, or composite key values as array.
+ * @return array matching record.
+ */
+ public function findByPk($keys)
+ {
+ if($keys===null)
+ return null;
+ list($where, $parameters) = $this->getPrimaryKeyCondition((array)$keys);
+ $command = $this->getBuilder()->createFindCommand($where, $parameters);
+ $this->onCreateCommand($command, new TSqlCriteria($where,$parameters));
+ return $this->onExecuteCommand($command, $command->queryRow());
+ }
+
+ /**
+ * @param array multiple primary key values or composite value arrays
+ * @return TDbDataReader record reader.
+ */
+ public function findAllByPk($keys)
+ {
+ $where = $this->getCompositeKeyCondition((array)$keys);
+ $command = $this->getBuilder()->createFindCommand($where);
+ $this->onCreateCommand($command, new TSqlCriteria($where,$keys));
+ return $this->onExecuteCommand($command,$command->query());
+ }
+
+ public function findAllByIndex($criteria,$fields,$values)
+ {
+ $index = $this->getIndexKeyCondition($this->getTableInfo(),$fields,$values);
+ if(strlen($where = $criteria->getCondition())>0)
+ $criteria->setCondition("({$index}) AND ({$where})");
+ else
+ $criteria->setCondition($index);
+ $command = $this->getFindCommand($criteria);
+ $this->onCreateCommand($command, $criteria);
+ return $this->onExecuteCommand($command,$command->query());
+ }
+
+ /**
+ * @param array multiple primary key values or composite value arrays
+ * @return integer number of rows affected.
+ */
+ public function deleteByPk($keys)
+ {
+ if(count($keys)==0)
+ return 0;
+ $where = $this->getCompositeKeyCondition((array)$keys);
+ $command = $this->getBuilder()->createDeleteCommand($where);
+ $this->onCreateCommand($command, new TSqlCriteria($where,$keys));
+ $command->prepare();
+ return $this->onExecuteCommand($command,$command->execute());
+ }
+
+ public function getIndexKeyCondition($table,$fields,$values)
+ {
+ if (!count($values))
+ return 'FALSE';
+ $columns = array();
+ $tableName = $table->getTableFullName();
+ foreach($fields as $field)
+ $columns[] = $tableName.'.'.$table->getColumn($field)->getColumnName();
+ return '('.implode(', ',$columns).') IN '.$this->quoteTuple($values);
+ }
+
+ /**
+ * Construct a "pk IN ('key1', 'key2', ...)" criteria.
+ * @param array values for IN predicate
+ * @param string SQL string for primary keys IN a list.
+ */
+ protected function getCompositeKeyCondition($values)
+ {
+ $primary = $this->getTableInfo()->getPrimaryKeys();
+ $count = count($primary);
+ if($count===0)
+ {
+ throw new TDbException('dbtablegateway_no_primary_key_found',
+ $this->getTableInfo()->getTableFullName());
+ }
+ if(!is_array($values) || count($values) === 0)
+ {
+ throw new TDbException('dbtablegateway_missing_pk_values',
+ $this->getTableInfo()->getTableFullName());
+ }
+ if($count>1 && (!isset($values[0]) || !is_array($values[0])))
+ $values = array($values);
+ if($count > 1 && count($values[0]) !== $count)
+ {
+ throw new TDbException('dbtablegateway_pk_value_count_mismatch',
+ $this->getTableInfo()->getTableFullName());
+ }
+ return $this->getIndexKeyCondition($this->getTableInfo(),$primary, $values);
+ }
+
+ /**
+ * @param TDbConnection database connection.
+ * @param array values
+ * @return string quoted recursive tuple values, e.g. "('val1', 'val2')".
+ */
+ protected function quoteTuple($array)
+ {
+ $conn = $this->getDbConnection();
+ $data = array();
+ foreach($array as $k=>$v)
+ $data[] = is_array($v) ? $this->quoteTuple($v) : $conn->quoteString($v);
+ return '('.implode(', ', $data).')';
+ }
+
+ /**
+ * Create the condition and parameters for find by primary.
+ * @param array primary key values
+ * @return array tuple($where, $parameters)
+ */
+ protected function getPrimaryKeyCondition($values)
+ {
+ $primary = $this->getTableInfo()->getPrimaryKeys();
+ if(count($primary)===0)
+ {
+ throw new TDbException('dbtablegateway_no_primary_key_found',
+ $this->getTableInfo()->getTableFullName());
+ }
+ $criteria=array();
+ $bindings=array();
+ $i = 0;
+ foreach($primary as $key)
+ {
+ $column = $this->getTableInfo()->getColumn($key)->getColumnName();
+ $criteria[] = $column.' = :'.$key;
+ $bindings[$key] = isset($values[$key])?$values[$key]:$values[$i++];
+ }
+ return array(implode(' AND ', $criteria), $bindings);
+ }
+
+ /**
+ * Find one matching records for arbituary SQL.
+ * @param TSqlCriteria $criteria
+ * @return TDbDataReader record reader.
+ */
+ public function findBySql($criteria)
+ {
+ $command = $this->getSqlCommand($criteria);
+ return $this->onExecuteCommand($command, $command->queryRow());
+ }
+
+ /**
+ * Find zero or more matching records for arbituary SQL.
+ * @param TSqlCriteria $criteria
+ * @return TDbDataReader record reader.
+ */
+ public function findAllBySql($criteria)
+ {
+ $command = $this->getSqlCommand($criteria);
+ return $this->onExecuteCommand($command, $command->query());
+ }
+
+ /**
+ * Build sql command from the criteria. Limit, Offset and Ordering are applied if applicable.
+ * @param TSqlCriteria $criteria
+ * @return TDbCommand command corresponding to the criteria.
+ */
+ protected function getSqlCommand($criteria)
+ {
+ $sql = $criteria->getCondition();
+ $ordering = $criteria->getOrdersBy();
+ $limit = $criteria->getLimit();
+ $offset = $criteria->getOffset();
+ if(count($ordering) > 0)
+ $sql = $this->getBuilder()->applyOrdering($sql, $ordering);
+ if($limit>=0 || $offset>=0)
+ $sql = $this->getBuilder()->applyLimitOffset($sql, $limit, $offset);
+ $command = $this->getBuilder()->createCommand($sql);
+ $this->getBuilder()->bindArrayValues($command, $criteria->getParameters()->toArray());
+ $this->onCreateCommand($command, $criteria);
+ return $command;
+ }
+
+ /**
+ * @param TSqlCriteria $criteria
+ * @return integer number of records.
+ */
+ public function count($criteria)
+ {
+ if($criteria===null)
+ return (int)$this->getBuilder()->createCountCommand()->queryScalar();
+ $where = $criteria->getCondition();
+ $parameters = $criteria->getParameters()->toArray();
+ $ordering = $criteria->getOrdersBy();
+ $limit = $criteria->getLimit();
+ $offset = $criteria->getOffset();
+ $command = $this->getBuilder()->createCountCommand($where,$parameters,$ordering,$limit,$offset);
+ $this->onCreateCommand($command, $criteria);
+ return $this->onExecuteCommand($command, (int)$command->queryScalar());
+ }
+
+ /**
+ * Inserts a new record into the table. Each array key must
+ * correspond to a column name in the table unless a null value is permitted.
+ * @param array new record data.
+ * @return mixed last insert id if one column contains a serial or sequence,
+ * otherwise true if command executes successfully and affected 1 or more rows.
+ */
+ public function insert($data)
+ {
+ $command=$this->getBuilder()->createInsertCommand($data);
+ $this->onCreateCommand($command, new TSqlCriteria(null,$data));
+ $command->prepare();
+ if($this->onExecuteCommand($command, $command->execute()) > 0)
+ {
+ $value = $this->getLastInsertId();
+ return $value !== null ? $value : true;
+ }
+ return false;
+ }
+
+ /**
+ * Iterate through all the columns and returns the last insert id of the
+ * first column that has a sequence or serial.
+ * @return mixed last insert id, null if none is found.
+ */
+ public function getLastInsertID()
+ {
+ return $this->getBuilder()->getLastInsertID();
+ }
+
+ /**
+ * @param string __call method name
+ * @param string criteria conditions
+ * @param array method arguments
+ * @return TActiveRecordCriteria criteria created from the method name and its arguments.
+ */
+ public function createCriteriaFromString($method, $condition, $args)
+ {
+ $fields = $this->extractMatchingConditions($method, $condition);
+ $args=count($args) === 1 && is_array($args[0]) ? $args[0] : $args;
+ if(count($fields)>count($args))
+ {
+ throw new TDbException('dbtablegateway_mismatch_args_exception',
+ $method,count($fields),count($args));
+ }
+ return new TSqlCriteria(implode(' ',$fields), $args);
+ }
+
+ /**
+ * Calculates the AND/OR condition from dynamic method substrings using
+ * table meta data, allows for any AND-OR combinations.
+ * @param string dynamic method name
+ * @param string dynamic method search criteria
+ * @return array search condition substrings
+ */
+ protected function extractMatchingConditions($method, $condition)
+ {
+ $table = $this->getTableInfo();
+ $columns = $table->getLowerCaseColumnNames();
+ $regexp = '/('.implode('|', array_keys($columns)).')(and|_and_|or|_or_)?/i';
+ $matches = array();
+ if(!preg_match_all($regexp, strtolower($condition), $matches,PREG_SET_ORDER))
+ {
+ throw new TDbException('dbtablegateway_mismatch_column_name',
+ $method, implode(', ', $columns), $table->getTableFullName());
+ }
+
+ $fields = array();
+ foreach($matches as $match)
+ {
+ $key = $columns[$match[1]];
+ $column = $table->getColumn($key)->getColumnName();
+ $sql = $column . ' = ? ';
+ if(count($match) > 2)
+ $sql .= strtoupper(str_replace('_', '', $match[2]));
+ $fields[] = $sql;
+ }
+ return $fields;
+ }
+
+ /**
+ * 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.
+ * @param TDataGatewayCommand originator $sender
+ * @param TDataGatewayEventParameter
+ */
+ public function onCreateCommand($command, $criteria)
+ {
+ $this->raiseEvent('OnCreateCommand', $this, new TDataGatewayEventParameter($command,$criteria));
+ }
+
+ /**
+ * 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.
+ * @param TDataGatewayCommand originator $sender
+ * @param TDataGatewayResultEventParameter
+ */
+ public function onExecuteCommand($command, $result)
+ {
+ $parameter = new TDataGatewayResultEventParameter($command, $result);
+ $this->raiseEvent('OnExecuteCommand', $this, $parameter);
+ return $parameter->getResult();
+ }
+}
+
+/**
+ * TDataGatewayEventParameter class contains the TDbCommand to be executed as
+ * well as the criteria object.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.DataGateway
+ * @since 3.1
+ */
+class TDataGatewayEventParameter extends TEventParameter
+{
+ private $_command;
+ private $_criteria;
+
+ public function __construct($command,$criteria)
+ {
+ $this->_command=$command;
+ $this->_criteria=$criteria;
+ }
+
+ /**
+ * The database command to be executed. Do not rebind the parameters or change
+ * the sql query string.
+ * @return TDbCommand command to be executed.
+ */
+ public function getCommand()
+ {
+ return $this->_command;
+ }
+
+ /**
+ * @return TSqlCriteria criteria used to bind the sql query parameters.
+ */
+ public function getCriteria()
+ {
+ return $this->_criteria;
+ }
+}
+
+/**
+ * TDataGatewayResultEventParameter contains the TDbCommand executed and the resulting
+ * data returned from the database. The data can be changed by changing the
+ * {@link setResult Result} property.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.DataGateway
+ * @since 3.1
+ */
+class TDataGatewayResultEventParameter extends TEventParameter
+{
+ private $_command;
+ private $_result;
+
+ public function __construct($command,$result)
+ {
+ $this->_command=$command;
+ $this->_result=$result;
+ }
+
+ /**
+ * @return TDbCommand database command executed.
+ */
+ public function getCommand()
+ {
+ return $this->_command;
+ }
+
+ /**
+ * @return mixed result returned from executing the command.
+ */
+ public function getResult()
+ {
+ return $this->_result;
+ }
+
+ /**
+ * @param mixed change the result returned by the gateway.
+ */
+ public function setResult($value)
+ {
+ $this->_result=$value;
+ }
+}
+
diff --git a/lib/prado/framework/Data/DataGateway/TSqlCriteria.php b/lib/prado/framework/Data/DataGateway/TSqlCriteria.php
new file mode 100644
index 0000000..3325c7f
--- /dev/null
+++ b/lib/prado/framework/Data/DataGateway/TSqlCriteria.php
@@ -0,0 +1,283 @@
+<?php
+/**
+ * TDbSqlCriteria 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.DataGateway
+ */
+
+/**
+ * Search criteria for TDbDataGateway.
+ *
+ * Criteria object for data gateway finder methods. Usage:
+ * <code>
+ * $criteria = new TSqlCriteria();
+ * $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.DataGateway
+ * @since 3.1
+ */
+class TSqlCriteria extends TComponent
+{
+ /**
+ * @var mixed
+ * @since 3.1.7
+ */
+ private $_select='*';
+ private $_condition;
+ private $_parameters;
+ private $_ordersBy;
+ private $_limit;
+ private $_offset;
+
+ /**
+ * Creates a new criteria with given condition;
+ * @param string sql string after the WHERE stanza
+ * @param mixed named or indexed parameters, accepts as multiple arguments.
+ */
+ public function __construct($condition=null, $parameters=array())
+ {
+ if(!is_array($parameters) && func_num_args() > 1)
+ $parameters = array_slice(func_get_args(),1);
+ $this->_parameters=new TAttributeCollection;
+ $this->_parameters->setCaseSensitive(true);
+ $this->_parameters->copyFrom((array)$parameters);
+ $this->_ordersBy=new TAttributeCollection;
+ $this->_ordersBy->setCaseSensitive(true);
+
+ $this->setCondition($condition);
+ }
+
+ /**
+ * Gets the field list to be placed after the SELECT in the SQL. Default to '*'
+ * @return mixed
+ * @since 3.1.7
+ */
+ public function getSelect()
+ {
+ return $this->_select;
+ }
+
+ /**
+ * Sets the field list to be placed after the SELECT in the SQL.
+ *
+ * Different behavior depends on type of assigned value
+ * string
+ * usage without modification
+ *
+ * null
+ * will be expanded to full list of quoted table column names (quoting depends on database)
+ *
+ * array
+ * - Column names will be quoted if used as key or value of array
+ * <code>
+ * array('col1', 'col2', 'col2')
+ * // SELECT `col1`, `col2`, `col3` FROM...
+ * </code>
+ *
+ * - Column aliasing
+ * <code>
+ * array('mycol1' => 'col1', 'mycol2' => 'COUNT(*)')
+ * // SELECT `col1` AS mycol1, COUNT(*) AS mycol2 FROM...
+ * </code>
+ *
+ * - NULL and scalar values (strings will be quoted depending on database)
+ * <code>
+ * array('col1' => 'my custom string', 'col2' => 1.0, 'col3' => 'NULL')
+ * // SELECT "my custom string" AS `col1`, 1.0 AS `col2`, NULL AS `col3` FROM...
+ * </code>
+ *
+ * - If the *-wildcard char is used as key or value, add the full list of quoted table column names
+ * <code>
+ * array('col1' => 'NULL', '*')
+ * // SELECT `col1`, `col2`, `col3`, NULL AS `col1` FROM...
+ * </code>
+ *
+ * @param mixed
+ * @since 3.1.7
+ * @see TDbCommandBuilder::getSelectFieldList()
+ */
+ public function setSelect($value)
+ {
+ $this->_select = $value;
+ }
+
+ /**
+ * @return string search conditions.
+ */
+ public function getCondition()
+ {
+ return $this->_condition;
+ }
+
+ /**
+ * Sets the search conditions to be placed after the WHERE clause in the SQL.
+ * @param string search conditions.
+ */
+ public function setCondition($value)
+ {
+ if(empty($value)) {
+ // reset the condition
+ $this->_condition = null;
+ return;
+ }
+
+ // supporting the following SELECT-syntax:
+ // [ORDER BY {col_name | expr | position}
+ // [ASC | DESC], ...]
+ // [LIMIT {[offset,] row_count | row_count OFFSET offset}]
+ // See: http://dev.mysql.com/doc/refman/5.0/en/select.html
+
+ if(preg_match('/ORDER\s+BY\s+(.*?)(?=LIMIT)|ORDER\s+BY\s+(.*?)$/i', $value, $matches) > 0) {
+ // condition contains ORDER BY
+ $value = str_replace($matches[0], '', $value);
+ if(strlen($matches[1]) > 0) {
+ $this->setOrdersBy($matches[1]);
+ } else if(strlen($matches[2]) > 0) {
+ $this->setOrdersBy($matches[2]);
+ }
+ }
+
+ if(preg_match('/LIMIT\s+([\d\s,]+)/i', $value, $matches) > 0) {
+ // condition contains limit
+ $value = str_replace($matches[0], '', $value); // remove limit from query
+ if(strpos($matches[1], ',')) { // both offset and limit given
+ list($offset, $limit) = explode(',', $matches[1]);
+ $this->_limit = (int)$limit;
+ $this->_offset = (int)$offset;
+ } else { // only limit given
+ $this->_limit = (int)$matches[1];
+ }
+ }
+
+ if(preg_match('/OFFSET\s+(\d+)/i', $value, $matches) > 0) {
+ // condition contains offset
+ $value = str_replace($matches[0], '', $value); // remove offset from query
+ $this->_offset = (int)$matches[1]; // set offset in criteria
+ }
+
+ $this->_condition = trim($value);
+ }
+
+ /**
+ * @return TAttributeCollection list of named parameters and values.
+ */
+ public function getParameters()
+ {
+ return $this->_parameters;
+ }
+
+ /**
+ * @param ArrayAccess named parameters.
+ */
+ public function setParameters($value)
+ {
+ if(!(is_array($value) || $value instanceof ArrayAccess))
+ throw new TException('value must be array or ArrayAccess');
+ $this->_parameters->copyFrom($value);
+ }
+
+ /**
+ * @return boolean true if the parameter index are string base, false otherwise.
+ */
+ public function getIsNamedParameters()
+ {
+ foreach($this->getParameters() as $k=>$v)
+ return is_string($k);
+ }
+
+ /**
+ * @return TAttributeCollection ordering clause.
+ */
+ public function getOrdersBy()
+ {
+ return $this->_ordersBy;
+ }
+
+ /**
+ * @param mixed ordering clause.
+ */
+ public function setOrdersBy($value)
+ {
+ if(is_array($value) || $value instanceof Traversable)
+ $this->_ordersBy->copyFrom($value);
+ else
+ {
+ $value=trim(preg_replace('/\s+/',' ',(string)$value));
+ $orderBys=array();
+ foreach(explode(',',$value) as $orderBy)
+ {
+ $vs=explode(' ',trim($orderBy));
+ $orderBys[$vs[0]]=isset($vs[1])?$vs[1]:'asc';
+ }
+ $this->_ordersBy->copyFrom($orderBys);
+ }
+ }
+
+ /**
+ * @return int maximum number of records to return.
+ */
+ public function getLimit()
+ {
+ return $this->_limit;
+ }
+
+ /**
+ * @param int maximum number of records to return.
+ */
+ public function setLimit($value)
+ {
+ $this->_limit=$value;
+ }
+
+ /**
+ * @return int record offset.
+ */
+ public function getOffset()
+ {
+ return $this->_offset;
+ }
+
+ /**
+ * @param int record offset.
+ */
+ public function setOffset($value)
+ {
+ $this->_offset=$value;
+ }
+
+ /**
+ * @return string string representation of the parameters. Useful for debugging.
+ */
+ public function __toString()
+ {
+ $str = '';
+ if(strlen((string)$this->getCondition()) > 0)
+ $str .= '"'.(string)$this->getCondition().'"';
+ $params = array();
+ foreach($this->getParameters() as $k=>$v)
+ $params[] = "{$k} => ${v}";
+ if(count($params) > 0)
+ $str .= ', "'.implode(', ',$params).'"';
+ $orders = array();
+ foreach($this->getOrdersBy() as $k=>$v)
+ $orders[] = "{$k} => ${v}";
+ if(count($orders) > 0)
+ $str .= ', "'.implode(', ',$orders).'"';
+ if($this->_limit !==null)
+ $str .= ', '.$this->_limit;
+ if($this->_offset !== null)
+ $str .= ', '.$this->_offset;
+ return $str;
+ }
+}
diff --git a/lib/prado/framework/Data/DataGateway/TTableGateway.php b/lib/prado/framework/Data/DataGateway/TTableGateway.php
new file mode 100644
index 0000000..0906d8d
--- /dev/null
+++ b/lib/prado/framework/Data/DataGateway/TTableGateway.php
@@ -0,0 +1,476 @@
+<?php
+/**
+ * TTableGateway 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.DataGateway
+ */
+
+/**
+ * Loads the data gateway command builder and sql criteria.
+ */
+Prado::using('System.Data.DataGateway.TSqlCriteria');
+Prado::using('System.Data.DataGateway.TDataGatewayCommand');
+
+/**
+ * TTableGateway class provides several find methods to get data from the database
+ * and update, insert, and delete methods.
+ *
+ * Each method maps the input parameters into a SQL call and executes the SQL
+ * against a database connection. The TTableGateway is stateless
+ * (with respect to the data and data objects), as its role is to push data back and forth.
+ *
+ * Example usage:
+ * <code>
+ * //create a connection
+ * $dsn = 'pgsql:host=localhost;dbname=test';
+ * $conn = new TDbConnection($dsn, 'dbuser','dbpass');
+ *
+ * //create a table gateway for table/view named 'address'
+ * $table = new TTableGateway('address', $conn);
+ *
+ * //insert a new row, returns last insert id (if applicable)
+ * $id = $table->insert(array('name'=>'wei', 'phone'=>'111111'));
+ *
+ * $record1 = $table->findByPk($id); //find inserted record
+ *
+ * //finds all records, returns an iterator
+ * $records = $table->findAll();
+ * print_r($records->readAll());
+ *
+ * //update the row
+ * $table->updateByPk($record1, $id);
+ * </code>
+ *
+ * All methods that may return more than one row of data will return an
+ * TDbDataReader iterator.
+ *
+ * The OnCreateCommand event is raised when a command is prepared and parameter
+ * binding is completed. The parameter object is a TDataGatewayEventParameter of which the
+ * {@link TDataGatewayEventParameter::getCommand Command} property can be
+ * inspected to obtain the sql query to be executed.
+ *
+ * The OnExecuteCommand event is raised when a command is executed and the result
+ * from the database was returned. The parameter object is a
+ * 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.
+ *
+ * <code>
+ * $table->OnCreateCommand[] = 'log_it'; //any valid PHP callback statement
+ * $table->OnExecuteCommand[] = array($obj, 'method_name'); // calls 'method_name' on $obj
+ *
+ * function log_it($sender, $param)
+ * {
+ * var_dump($param); //TDataGatewayEventParameter object.
+ * }
+ * </code>
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @version $Id$
+ * @package System.Data.DataGateway
+ * @since 3.1
+ */
+class TTableGateway extends TComponent
+{
+ private $_command;
+ private $_connection;
+
+ /**
+ * Creates a new generic table gateway for a given table or view name
+ * and a database connection.
+ * @param string|TDbTableInfo table or view name or table information.
+ * @param TDbConnection database connection.
+ */
+ public function __construct($table,$connection)
+ {
+ $this->_connection=$connection;
+ if(is_string($table))
+ $this->setTableName($table);
+ else if($table instanceof TDbTableInfo)
+ $this->setTableInfo($table);
+ else
+ throw new TDbException('dbtablegateway_invalid_table_info');
+ }
+
+ /**
+ * @param TDbTableInfo table or view information.
+ */
+ protected function setTableInfo($tableInfo)
+ {
+ $builder = $tableInfo->createCommandBuilder($this->getDbConnection());
+ $this->initCommandBuilder($builder);
+ }
+
+ /**
+ * Sets up the command builder for the given table.
+ * @param string table or view name.
+ */
+ protected function setTableName($tableName)
+ {
+ Prado::using('System.Data.Common.TDbMetaData');
+ $meta = TDbMetaData::getInstance($this->getDbConnection());
+ $this->initCommandBuilder($meta->createCommandBuilder($tableName));
+ }
+
+ public function getTableInfo()
+ {
+ return $this->getCommand()->getTableInfo();
+ }
+
+ public function getTableName()
+ {
+ return $this->getTableInfo()->getTableName();
+ }
+
+ /**
+ * @param TDbCommandBuilder database specific command builder.
+ */
+ protected function initCommandBuilder($builder)
+ {
+ $this->_command = new TDataGatewayCommand($builder);
+ $this->_command->OnCreateCommand[] = array($this, 'onCreateCommand');
+ $this->_command->OnExecuteCommand[] = array($this, 'onExecuteCommand');
+ }
+
+ /**
+ * 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.
+ * @param TDataGatewayCommand originator $sender
+ * @param TDataGatewayEventParameter
+ */
+ public function onCreateCommand($sender, $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.
+ * @param TDataGatewayCommand originator $sender
+ * @param TDataGatewayResultEventParameter
+ */
+ public function onExecuteCommand($sender, $param)
+ {
+ $this->raiseEvent('OnExecuteCommand', $this, $param);
+ }
+
+ /**
+ * @return TDataGatewayCommand command builder and executor.
+ */
+ protected function getCommand()
+ {
+ return $this->_command;
+ }
+
+ /**
+ * @return TDbConnection database connection.
+ */
+ public function getDbConnection()
+ {
+ return $this->_connection;
+ }
+
+ /**
+ * Execute arbituary sql command with binding parameters.
+ * @param string SQL query string.
+ * @param array binding parameters, positional or named.
+ * @return array query results.
+ */
+ public function findBySql($sql, $parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ $criteria = $this->getCriteria($sql,$parameters, $args);
+ return $this->getCommand()->findBySql($criteria);
+ }
+
+ /**
+ * Execute arbituary sql command with binding parameters.
+ * @param string SQL query string.
+ * @param array binding parameters, positional or named.
+ * @return TDbDataReader query results.
+ */
+ public function findAllBySql($sql, $parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ $criteria = $this->getCriteria($sql,$parameters, $args);
+ return $this->getCommand()->findAllBySql($criteria);
+ }
+
+ /**
+ * Find one single record that matches the criteria.
+ *
+ * Usage:
+ * <code>
+ * $table->find('username = :name AND password = :pass',
+ * array(':name'=>$name, ':pass'=>$pass));
+ * $table->find('username = ? AND password = ?', array($name, $pass));
+ * $table->find('username = ? AND password = ?', $name, $pass);
+ * //$criteria is of TSqlCriteria
+ * $table->find($criteria); //the 2nd parameter for find() is ignored.
+ * </code>
+ *
+ * @param string|TSqlCriteria SQL condition or criteria object.
+ * @param mixed parameter values.
+ * @return array matching record object.
+ */
+ public function find($criteria, $parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ $criteria = $this->getCriteria($criteria,$parameters, $args);
+ return $this->getCommand()->find($criteria);
+ }
+
+ /**
+ * Accepts same parameters as find(), but returns TDbDataReader instead.
+ * @param string|TSqlCriteria SQL condition or criteria object.
+ * @param mixed parameter values.
+ * @return TDbDataReader matching records.
+ */
+ public function findAll($criteria=null, $parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ if($criteria!==null)
+ $criteria = $this->getCriteria($criteria,$parameters, $args);
+ return $this->getCommand()->findAll($criteria);
+ }
+
+ /**
+ * Find one record using only the primary key or composite primary keys. Usage:
+ *
+ * <code>
+ * $table->findByPk($primaryKey);
+ * $table->findByPk($key1, $key2, ...);
+ * $table->findByPk(array($key1,$key2,...));
+ * </code>
+ *
+ * @param mixed primary keys
+ * @return array matching record.
+ */
+ public function findByPk($keys)
+ {
+ if(func_num_args() > 1)
+ $keys = func_get_args();
+ return $this->getCommand()->findByPk($keys);
+ }
+
+ /**
+ * Similar to findByPk(), but returns TDbDataReader instead.
+ *
+ * For scalar primary keys:
+ * <code>
+ * $table->findAllByPk($key1, $key2, ...);
+ * $table->findAllByPk(array($key1, $key2, ...));
+ * </code>
+ *
+ * For composite keys:
+ * <code>
+ * $table->findAllByPk(array($key1, $key2), array($key3, $key4), ...);
+ * $table->findAllByPk(array(array($key1, $key2), array($key3, $key4), ...));
+ * </code>
+ * @param mixed primary keys
+ * @return TDbDataReader data reader.
+ */
+ public function findAllByPks($keys)
+ {
+ if(func_num_args() > 1)
+ $keys = func_get_args();
+ return $this->getCommand()->findAllByPk($keys);
+ }
+
+ /**
+ * Delete records from the table with condition given by $where and
+ * binding values specified by $parameter argument.
+ * This method uses additional arguments as $parameters. E.g.
+ * <code>
+ * $table->delete('age > ? AND location = ?', $age, $location);
+ * </code>
+ * @param string delete condition.
+ * @param array condition parameters.
+ * @return integer number of records deleted.
+ */
+ public function deleteAll($criteria, $parameters=array())
+ {
+ $args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
+ $criteria = $this->getCriteria($criteria,$parameters, $args);
+ return $this->getCommand()->delete($criteria);
+ }
+
+ /**
+ * Delete records by primary key. Usage:
+ *
+ * <code>
+ * $table->deleteByPk($primaryKey); //delete 1 record
+ * $table->deleteByPk($key1,$key2,...); //delete multiple records
+ * $table->deleteByPk(array($key1,$key2,...)); //delete multiple records
+ * </code>
+ *
+ * For composite primary keys (determined from the table definitions):
+ * <code>
+ * $table->deleteByPk(array($key1,$key2)); //delete 1 record
+ *
+ * //delete multiple records
+ * $table->deleteByPk(array($key1,$key2), array($key3,$key4),...);
+ *
+ * //delete multiple records
+ * $table->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->getCommand()->deleteByPk($keys);
+ }
+
+ /**
+ * Alias for deleteByPk()
+ */
+ public function deleteAllByPks($keys)
+ {
+ if(func_num_args() > 1)
+ $keys = func_get_args();
+ return $this->deleteByPk($keys);
+ }
+
+ /**
+ * Find the number of records.
+ * @param string|TSqlCriteria 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->getCriteria($criteria,$parameters, $args);
+ return $this->getCommand()->count($criteria);
+ }
+
+ /**
+ * Updates the table with new name-value pair $data. Each array key must
+ * correspond to a column name in the table. The update condition is
+ * specified by the $where argument and additional binding values can be
+ * specified using the $parameter argument.
+ * This method uses additional arguments as $parameters. E.g.
+ * <code>
+ * $gateway->update($data, 'age > ? AND location = ?', $age, $location);
+ * </code>
+ * @param array new record data.
+ * @param string update condition
+ * @param array additional binding name-value pairs.
+ * @return integer number of records updated.
+ */
+ public function update($data, $criteria, $parameters=array())
+ {
+ $args = func_num_args() > 2 ? array_slice(func_get_args(),2) : null;
+ $criteria = $this->getCriteria($criteria,$parameters, $args);
+ return $this->getCommand()->update($data, $criteria);
+ }
+
+ /**
+ * Inserts a new record into the table. Each array key must
+ * correspond to a column name in the table unless a null value is permitted.
+ * @param array new record data.
+ * @return mixed last insert id if one column contains a serial or sequence,
+ * otherwise true if command executes successfully and affected 1 or more rows.
+ */
+ public function insert($data)
+ {
+ return $this->getCommand()->insert($data);
+ }
+
+ /**
+ * @return mixed last insert id, null if none is found.
+ */
+ public function getLastInsertId()
+ {
+ return $this->getCommand()->getLastInsertId();
+ }
+
+ /**
+ * 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 getCriteria($criteria, $parameters, $args)
+ {
+ if(is_string($criteria))
+ {
+ $useArgs = !is_array($parameters) && is_array($args);
+ return new TSqlCriteria($criteria,$useArgs ? $args : $parameters);
+ }
+ else if($criteria instanceof TSqlCriteria)
+ return $criteria;
+ else
+ throw new TDbException('dbtablegateway_invalid_criteria');
+ }
+
+ /**
+ * 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>
+ * $table->findByName($name)
+ * $table->find('Name = ?', $name);
+ * </code>
+ * <code>
+ * $table->findByUsernameAndPassword($name,$pass); // OR may be used
+ * $table->findBy_Username_And_Password($name,$pass); // _OR_ may be used
+ * $table->find('Username = ? AND Password = ?', $name, $pass);
+ * </code>
+ * <code>
+ * $table->findAllByAge($age);
+ * $table->findAll('Age = ?', $age);
+ * </code>
+ * <code>
+ * $table->deleteAll('Name = ?', $name);
+ * $table->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($findOne = substr(strtolower($method),0,6)==='findby')
+ $condition = $method[6]==='_' ? substr($method,7) : substr($method,6);
+ else if(substr(strtolower($method),0,9)==='findallby')
+ $condition = $method[9]==='_' ? substr($method,10) : substr($method,9);
+ else if($delete = substr(strtolower($method),0,8)==='deleteby')
+ $condition = $method[8]==='_' ? substr($method,9) : substr($method,8);
+ else if($delete = substr(strtolower($method),0,11)==='deleteallby')
+ $condition = $method[11]==='_' ? substr($method,12) : substr($method,11);
+ else
+ return null;
+
+ $criteria = $this->getCommand()->createCriteriaFromString($method, $condition, $args);
+ if($delete)
+ return $this->deleteAll($criteria);
+ else
+ return $findOne ? $this->find($criteria) : $this->findAll($criteria);
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TDiscriminator.php b/lib/prado/framework/Data/SqlMap/Configuration/TDiscriminator.php
new file mode 100644
index 0000000..004bcca
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TDiscriminator.php
@@ -0,0 +1,229 @@
+<?php
+/**
+ * TDiscriminator and TSubMap 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.SqlMap.Configuration
+ */
+
+/**
+ * The TDiscriminator corresponds to the <discriminator> tag within a <resultMap>.
+ *
+ * TDiscriminator allows inheritance logic in SqlMap result mappings.
+ * SqlMap compares the data found in the discriminator column to the different
+ * <submap> values using the column value's string equivalence. When the string values
+ * matches a particular <submap>, SqlMap will use the <resultMap> defined by
+ * {@link resultMapping TSubMap::setResultMapping()} property for loading
+ * the object data.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TDiscriminator extends TComponent
+{
+ private $_column;
+ private $_type;
+ private $_typeHandler=null;
+ private $_columnIndex;
+ private $_nullValue;
+ private $_mapping;
+ private $_resultMaps=array();
+ private $_subMaps=array();
+
+ /**
+ * @return string the name of the column in the result set from which the
+ * value will be used to populate the property.
+ */
+ public function getColumn()
+ {
+ return $this->_column;
+ }
+
+ /**
+ * @param string the name of the column in the result set from which the
+ * value will be used to populate the property.
+ */
+ public function setColumn($value)
+ {
+ $this->_column = $value;
+ }
+
+ /**
+ * @param string property type of the parameter to be set.
+ */
+ public function getType()
+ {
+ return $this->_type;
+ }
+
+ /**
+ * The type attribute is used to explicitly specify the property type of the
+ * parameter to be set. If the attribute type is not set and the framework
+ * cannot otherwise determine the type, the type is assumed from the default
+ * value of the property.
+ * @return string property type of the parameter to be set.
+ */
+ public function setType($value)
+ {
+ $this->_type = $value;
+ }
+
+ /**
+ * @return string custom type handler class name (may use namespace).
+ */
+ public function getTypeHandler()
+ {
+ return $this->_typeHandler;
+ }
+
+ /**
+ * @param string custom type handler class name (may use namespace).
+ */
+ public function setTypeHandler($value)
+ {
+ $this->_typeHandler = $value;
+ }
+
+ /**
+ * @return int index of the column in the ResultSet
+ */
+ public function getColumnIndex()
+ {
+ return $this->_columnIndex;
+ }
+
+ /**
+ * The columnIndex attribute value is the index of the column in the
+ * ResultSet from which the value will be used to populate the object property.
+ * @param int index of the column in the ResultSet
+ */
+ public function setColumnIndex($value)
+ {
+ $this->_columnIndex = TPropertyValue::ensureInteger($value);
+ }
+
+ /**
+ * @return mixed outgoing null value replacement.
+ */
+ public function getNullValue()
+ {
+ return $this->_nullValue;
+ }
+
+ /**
+ * @param mixed outgoing null value replacement.
+ */
+ public function setNullValue($value)
+ {
+ $this->_nullValue = $value;
+ }
+
+ /**
+ * @return TResultProperty result property for the discriminator column.
+ */
+ public function getMapping()
+ {
+ return $this->_mapping;
+ }
+
+ /**
+ * @param TSubMap add new sub mapping.
+ */
+ public function addSubMap($subMap)
+ {
+ $this->_subMaps[] = $subMap;
+ }
+
+ /**
+ * @param string database value
+ * @return TResultMap result mapping.
+ */
+ public function getSubMap($value)
+ {
+ if(isset($this->_resultMaps[$value]))
+ return $this->_resultMaps[$value];
+ }
+
+ /**
+ * Copies the discriminator properties to a new TResultProperty.
+ * @param TResultMap result map holding the discriminator.
+ */
+ public function initMapping($resultMap)
+ {
+ $this->_mapping = new TResultProperty($resultMap);
+ $this->_mapping->setColumn($this->getColumn());
+ $this->_mapping->setColumnIndex($this->getColumnIndex());
+ $this->_mapping->setType($this->getType());
+ $this->_mapping->setTypeHandler($this->getTypeHandler());
+ $this->_mapping->setNullValue($this->getNullValue());
+ }
+
+ /**
+ * Set the result maps for particular sub-mapping values.
+ * @param TSqlMapManager sql map manager instance.
+ */
+ public function initialize($manager)
+ {
+ foreach($this->_subMaps as $subMap)
+ {
+ $this->_resultMaps[$subMap->getValue()] =
+ $manager->getResultMap($subMap->getResultMapping());
+ }
+ }
+}
+
+/**
+ * TSubMap class defines a submapping value and the corresponding <resultMap>
+ *
+ * The {@link Value setValue()} property is used for comparison with the
+ * discriminator column value. When the {@link Value setValue()} matches
+ * that of the discriminator column value, the corresponding {@link ResultMapping setResultMapping}
+ * is used inplace of the current result map.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSubMap extends TComponent
+{
+ private $_value;
+ private $_resultMapping;
+
+ /**
+ * @return string value for comparison with discriminator column value.
+ */
+ public function getValue()
+ {
+ return $this->_value;
+ }
+
+ /**
+ * @param string value for comparison with discriminator column value.
+ */
+ public function setValue($value)
+ {
+ $this->_value = $value;
+ }
+
+ /**
+ * The result map to use when the Value matches the discriminator column value.
+ * @return string ID of a result map
+ */
+ public function getResultMapping()
+ {
+ return $this->_resultMapping;
+ }
+
+ /**
+ * @param string ID of a result map
+ */
+ public function setResultMapping($value)
+ {
+ $this->_resultMapping = $value;
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TInlineParameterMapParser.php b/lib/prado/framework/Data/SqlMap/Configuration/TInlineParameterMapParser.php
new file mode 100644
index 0000000..dfe14f8
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TInlineParameterMapParser.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * TInlineParameterMapParser 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.SqlMap.Configuration
+ */
+
+/**
+ * TInlineParameterMapParser class.
+ *
+ * The inline parameter map syntax lets you embed the property name,
+ * the property type, the column type, and a null value replacement into a
+ * parametrized SQL statement.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TInlineParameterMapParser
+{
+ /**
+ * Regular expression for parsing inline parameter maps.
+ */
+ const PARAMETER_TOKEN_REGEXP = '/#([^#]+)#/';
+
+ /**
+ * Parse the sql text for inline parameters.
+ * @param string sql text
+ * @param array file and node details for exception message.
+ * @return array 'sql' and 'parameters' name value pairs.
+ */
+ public function parse($sqlText, $scope)
+ {
+ $matches = array();
+ $mappings = array();
+ preg_match_all(self::PARAMETER_TOKEN_REGEXP, $sqlText, $matches);
+
+ for($i = 0, $k=count($matches[1]); $i<$k; $i++)
+ {
+ $mappings[] = $this->parseMapping($matches[1][$i], $scope);
+ $sqlText = str_replace($matches[0][$i], '?', $sqlText);
+ }
+ return array('sql'=>$sqlText, 'parameters'=>$mappings);
+ }
+
+ /**
+ * Parse inline parameter with syntax as
+ * #propertyName,type=string,dbype=Varchar,nullValue=N/A,handler=string#
+ * @param string parameter token
+ * @param array file and node details for exception message.
+ */
+ protected function parseMapping($token, $scope)
+ {
+ $mapping = new TParameterProperty;
+ $properties = explode(',', $token);
+ $mapping->setProperty(trim(array_shift($properties)));
+ foreach($properties as $property)
+ {
+ $prop = explode('=',$property);
+ $name = trim($prop[0]); $value=trim($prop[1]);
+ if($mapping->canSetProperty($name))
+ $mapping->{'set'.$name}($value);
+ else
+ {
+ throw new TSqlMapUndefinedException(
+ 'sqlmap_undefined_property_inline_map',
+ $name, $scope['file'], $scope['node'], $token);
+ }
+ }
+ return $mapping;
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TParameterMap.php b/lib/prado/framework/Data/SqlMap/Configuration/TParameterMap.php
new file mode 100644
index 0000000..d6f90b2
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TParameterMap.php
@@ -0,0 +1,208 @@
+<?php
+/**
+ * TParameterMap 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.SqlMap.Configuration
+ */
+
+/**
+ * TParameterMap corresponds to the <parameterMap> element.
+ *
+ * TParameterMap holds one or more parameter child elements that map object
+ * properties to placeholders in a SQL statement.
+ *
+ * A TParameterMap defines an ordered list of values that match up with the
+ * placeholders of a parameterized query statement. While the attributes
+ * specified by the map still need to be in the correct order, each parameter
+ * is named. You can populate the underlying class in any order, and the
+ * TParameterMap ensures each value is passed in the correct order.
+ *
+ * Parameter Maps can be provided as an external element and inline.
+ * The <parameterMap> element accepts two attributes: id (required) and extends (optional).
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TParameterMap extends TComponent
+{
+ private $_extend;
+ private $_properties;
+ private $_propertyMap;
+ private $_extendMap;
+ private $_ID;
+
+ /**
+ * Initialize the properties and property map collections.
+ */
+ public function __construct()
+ {
+ $this->_properties = new TList;
+ $this->_propertyMap = new TMap;
+ }
+
+ /**
+ * @return string a unique identifier for the <parameterMap>.
+ */
+ public function getID()
+ {
+ return $this->_ID;
+ }
+
+ /**
+ * @param string a unique identifier for the <parameterMap>.
+ */
+ public function setID($value)
+ {
+ $this->_ID=$value;
+ }
+
+ /**
+ * @return TParameterProperty[] list of properties for the parameter map.
+ */
+ public function getProperties()
+ {
+ return $this->_properties;
+ }
+
+ /**
+ * @return string name of another <parameterMap> upon which to base this TParameterMap.
+ */
+ public function getExtends()
+ {
+ return $this->_extend;
+ }
+
+ /**
+ * @param string name of another <parameterMap> upon which to base this TParameterMap.
+ */
+ public function setExtends($value)
+ {
+ $this->_extend = $value;
+ }
+
+ /**
+ * @param string name of a parameter property.
+ * @return TParameterProperty parameter property.
+ * @throws TSqlMapException if index is not string nor integer.
+ */
+ public function getProperty($index)
+ {
+ if(is_string($index))
+ return $this->_propertyMap->itemAt($index);
+ else if(is_int($index))
+ return $this->_properties->itemAt($index);
+ else
+ throw new TSqlMapException('sqlmap_index_must_be_string_or_int', $index);
+ }
+
+ /**
+ * @param TParameterProperty new parameter property
+ */
+ public function addProperty(TParameterProperty $property)
+ {
+ $this->_propertyMap->add($property->getProperty(), $property);
+ $this->_properties->add($property);
+ }
+
+ /**
+ * @param int parameter property index
+ * @param TParameterProperty new parameter property.
+ */
+ public function insertProperty($index, TParameterProperty $property)
+ {
+ $this->_propertyMap->add($property->getProperty(), $property);
+ $this->_properties->insertAt($index, $property);
+ }
+
+ /**
+ * @return array list of property names.
+ */
+ public function getPropertyNames()
+ {
+ return $this->_propertyMap->getKeys();
+ }
+
+ /**
+ * Get the value of a property from the the parameter object.
+ * @param TSqlMapTypeHandlerRegistry type handler registry.
+ * @param TParameterProperty parameter proproperty.
+ * @param mixed parameter object to get the value from.
+ * @return unknown
+ */
+ public function getPropertyValue($registry, $property, $parameterValue)
+ {
+ $value = $this->getObjectValue($parameterValue,$property);
+
+ if(($handler=$this->createTypeHandler($property, $registry))!==null)
+ $value = $handler->getParameter($value);
+
+ $value = $this->nullifyDefaultValue($property,$value);
+
+ if(($type = $property->getType())!==null)
+ $value = $registry->convertToType($type, $value);
+
+ return $value;
+ }
+
+
+ /**
+ * Create type handler from {@link Type setType()} or {@link TypeHandler setTypeHandler}.
+ * @param TParameterProperty parameter property
+ * @param TSqlMapTypeHandlerRegistry type handler registry
+ * @return TSqlMapTypeHandler type handler.
+ */
+ protected function createTypeHandler($property, $registry)
+ {
+ $type=$property->getTypeHandler() ? $property->getTypeHandler() : $property->getType();
+ $handler=$registry->getTypeHandler($type);
+ if($handler===null && $property->getTypeHandler())
+ $handler = Prado::createComponent($type);
+ return $handler;
+ }
+
+
+ /**
+ * @param mixed object to obtain the property from.
+ * @param TParameterProperty parameter property.
+ * @return mixed property value.
+ * @throws TSqlMapException if property access is invalid.
+ */
+ protected function getObjectValue($object,$property)
+ {
+ try
+ {
+ return TPropertyAccess::get($object, $property->getProperty());
+ }
+ catch (TInvalidPropertyException $e)
+ {
+ throw new TSqlMapException(
+ 'sqlmap_unable_to_get_property_for_parameter',
+ $this->getID(),
+ $property->getProperty(),
+ (is_object($object) ? get_class($object) : gettype($object))
+ );
+ }
+ }
+
+ /**
+ * When the actual value matches the {@link NullValue TParameterProperty::setNullValue()},
+ * set the current value to null.
+ * @param TParameterProperty parameter property.
+ * @param mixed current property value
+ * @return mixed null if NullValue matches currrent value.
+ */
+ protected function nullifyDefaultValue($property,$value)
+ {
+ if(($nullValue = $property->getNullValue())!==null)
+ {
+ if($nullValue === $value)
+ $value = null;
+ }
+ return $value;
+ }
+}
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TParameterProperty.php b/lib/prado/framework/Data/SqlMap/Configuration/TParameterProperty.php
new file mode 100644
index 0000000..f6282e4
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TParameterProperty.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * TParameterPropert 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.SqlMap.Configuration
+ */
+
+/**
+ * TParameterProperty corresponds to the <property> tag and defines
+ * one object property for the <parameterMap>
+ *
+ * The {@link NullValue setNullValue()} attribute can be set to any valid
+ * value (based on property type). The {@link NullValue setNullValue()} attribute
+ * is used to specify an inbound null value replacement. What this means is
+ * that when the value is detected in the object property, a NULL will be written
+ * to the database (the opposite behavior of an inbound null value replacement).
+ * This allows you to use a magic null number in your application for types that
+ * do not support null values (such as int, double, float). When these types of
+ * properties contain a matching null value (for example, say, -9999), a NULL
+ * will be written to the database instead of the value.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TParameterProperty extends TComponent
+{
+ private $_typeHandler;
+ private $_type;
+ private $_column;
+ private $_dbType;
+ private $_property;
+ private $_nullValue;
+
+ /**
+ * @return string class name of a custom type handler.
+ */
+ public function getTypeHandler()
+ {
+ return $this->_typeHandler;
+ }
+
+ /**
+ * @param string class name of a custom type handler.
+ */
+ public function setTypeHandler($value)
+ {
+ $this->_typeHandler = $value;
+ }
+
+ /**
+ * @return string type of the parameter's property
+ */
+ public function getType()
+ {
+ return $this->_type;
+ }
+
+ /**
+ * @param string type of the parameter's property
+ */
+ public function setType($value)
+ {
+ $this->_type = $value;
+ }
+
+ /**
+ * @return string name of a parameter to be used in the SQL statement.
+ */
+ public function getColumn()
+ {
+ return $this->_column;
+ }
+
+ /**
+ * @param string name of a parameter to be used in the SQL statement.
+ */
+ public function setColumn($value)
+ {
+ $this->_column = $value;
+ }
+
+ /**
+ * @return string the database column type of the parameter to be set by this property.
+ */
+ public function getDbType()
+ {
+ return $this->_dbType;
+ }
+
+ /**
+ * @param string the database column type of the parameter to be set by this property.
+ */
+ public function setDbType($value)
+ {
+ $this->_dbType = $value;
+ }
+
+ /**
+ * @return string name of a property of the parameter object.
+ */
+ public function getProperty()
+ {
+ return $this->_property;
+ }
+
+ /**
+ * @param string name of a property of the parameter object.
+ */
+ public function setProperty($value)
+ {
+ $this->_property = $value;
+ }
+
+ /**
+ * @return mixed null value replacement
+ */
+ public function getNullValue()
+ {
+ return $this->_nullValue;
+ }
+
+ /**
+ * The nullValue attribute is used to specify an outgoing null value replacement.
+ * @param mixed null value replacement.
+ */
+ public function setNullValue($value)
+ {
+ $this->_nullValue = $value;
+ }
+
+ public function __sleep()
+ {
+ $exprops = array(); $cn = 'TParameterProperty';
+ if ($this->_typeHandler===null) $exprops[] = "\0$cn\0_typeHandler";
+ if ($this->_type===null) $exprops[] = "\0$cn\0_type";
+ if ($this->_column===null) $exprops[] = "\0$cn\0_column";
+ if ($this->_dbType===null) $exprops[] = "\0$cn\0_dbType";
+ if ($this->_property===null) $exprops[] = "\0$cn\0_property";
+ if ($this->_nullValue===null) $exprops[] = "\0$cn\0_nullValue";
+ return array_diff(parent::__sleep(),$exprops);
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TResultMap.php b/lib/prado/framework/Data/SqlMap/Configuration/TResultMap.php
new file mode 100644
index 0000000..95e8d34
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TResultMap.php
@@ -0,0 +1,198 @@
+<?php
+/**
+ * TResultMap 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.SqlMap.Configuration
+ */
+
+/**
+ * TResultMap corresponds to <resultMap> mapping tag.
+ *
+ * A TResultMap lets you control how data is extracted from the result of a
+ * query, and how the columns are mapped to object properties. A TResultMap
+ * can describe the column type, a null value replacement, and complex property
+ * mappings including Collections.
+ *
+ * The <resultMap> can contain any number of property mappings that map object
+ * properties to the columns of a result element. The property mappings are
+ * applied, and the columns are read, in the order that they are defined.
+ * Maintaining the element order ensures consistent results between different
+ * drivers and providers.
+ *
+ * The {@link Class setClass()} property must be a PHP class object or array instance.
+ *
+ * The optional {@link Extends setExtends()} attribute can be set to the ID of
+ * another <resultMap> upon which to base this <resultMap>. All properties of the
+ * "parent" <resultMap> will be included as part of this <resultMap>, and values
+ * from the "parent" <resultMap> are set before any values specified by this <resultMap>.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TResultMap extends TComponent
+{
+ private $_columns;
+ private $_class;
+ private $_extends;
+ private $_groupBy;
+ private $_discriminator;
+ private $_typeHandlers;
+ private $_ID;
+
+ /**
+ * Initialize the columns collection.
+ */
+ public function __construct()
+ {
+ $this->_columns=new TMap;
+ }
+
+ /**
+ * @return string a unique identifier for the <resultMap>.
+ */
+ public function getID()
+ {
+ return $this->_ID;
+ }
+
+ /**
+ * @param string a unique identifier for the <resultMap>.
+ */
+ public function setID($value)
+ {
+ $this->_ID=$value;
+ }
+
+ /**
+ * @return string result class name.
+ */
+ public function getClass()
+ {
+ return $this->_class;
+ }
+
+ /**
+ * @param string result class name.
+ */
+ public function setClass($value)
+ {
+ $this->_class = $value;
+ }
+
+ /**
+ * @return TMap result columns.
+ */
+ public function getColumns()
+ {
+ return $this->_columns;
+ }
+
+ /**
+ * @return string result map extends another result map.
+ */
+ public function getExtends()
+ {
+ return $this->_extends;
+ }
+
+ /**
+ * @param string result map extends another result map.
+ */
+ public function setExtends($value)
+ {
+ $this->_extends = $value;
+ }
+
+ /**
+ * @return string result map groups by.
+ */
+ public function getGroupBy()
+ {
+ return $this->_groupBy;
+ }
+
+ /**
+ * @param string result map group by
+ */
+ public function setGroupBy($value)
+ {
+ $this->_groupBy = $value;
+ }
+
+ /**
+ * @return TDiscriminator result class discriminator.
+ */
+ public function getDiscriminator()
+ {
+ return $this->_discriminator;
+ }
+
+ /**
+ * @param TDiscriminator result class discriminator.
+ */
+ public function setDiscriminator(TDiscriminator $value)
+ {
+ $this->_discriminator = $value;
+ }
+
+ /**
+ * Add a TResultProperty to result mapping.
+ * @param TResultProperty result property.
+ */
+ public function addResultProperty(TResultProperty $property)
+ {
+ $this->_columns[$property->getProperty()] = $property;
+ }
+
+ /**
+ * Create a new instance of the class of this result map.
+ * @param TSqlMapTypeHandlerRegistry type handler registry.
+ * @return mixed new result object.
+ * @throws TSqlMapException
+ */
+ public function createInstanceOfResult($registry)
+ {
+ $handler = $registry->getTypeHandler($this->getClass());
+ try
+ {
+ if($handler!==null)
+ return $handler->createNewInstance();
+ else
+ return $registry->createInstanceOf($this->getClass());
+ }
+ catch (TSqlMapException $e)
+ {
+ throw new TSqlMapException(
+ 'sqlmap_unable_to_create_new_instance',
+ $this->getClass(), get_class($handler), $this->getID());
+ }
+ }
+
+ /**
+ * Result sub-mappings using the discriminiator column.
+ * @param TSqlMapTypeHandlerRegistry type handler registry
+ * @param array row data.
+ * @return TResultMap result sub-map.
+ */
+ public function resolveSubMap($registry,$row)
+ {
+ $subMap = $this;
+ if(($disc = $this->getDiscriminator())!==null)
+ {
+ $value = $disc->getMapping()->getPropertyValue($registry,$row);
+ $subMap = $disc->getSubMap((string)$value);
+
+ if($subMap===null)
+ $subMap = $this;
+ else if($subMap !== $this)
+ $subMap = $subMap->resolveSubMap($registry,$row);
+ }
+ return $subMap;
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TResultProperty.php b/lib/prado/framework/Data/SqlMap/Configuration/TResultProperty.php
new file mode 100644
index 0000000..87b0677
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TResultProperty.php
@@ -0,0 +1,342 @@
+<?php
+/**
+ * TResultProperty 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.SqlMap.Configuration
+ */
+
+/**
+ * TResultProperty corresponds a <property> tags inside a <resultMap> tag.
+ *
+ * The {@link NullValue setNullValue()} attribute can be set to any valid
+ * value (based on property type). The {@link NullValue setNullValue()} attribute
+ * is used to specify an outgoing null value replacement. What this means is
+ * that when a null value is detected in the result, the corresponding value of
+ * the {@link NullValue getNullValue()} will be used instead.
+ *
+ * The {@link Select setSelect()} property is used to describe a relationship
+ * between objects and to automatically load complex (i.e. user defined)
+ * property types. The value of the {@link Select setSelect()} property must be
+ * the name of another mapped statement. The value of the database
+ * {@link Column setColumn()} that is defined in the same property element as
+ * this statement attribute will be passed to the related mapped statement as
+ * the parameter. The {@link LazyLoad setLayLoad()} attribute can be specified
+ * with the {@link Select setSelect()} .
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TResultProperty extends TComponent
+{
+ private $_nullValue;
+ private $_propertyName;
+ private $_columnName;
+ private $_columnIndex=-1;
+ private $_nestedResultMapName;
+ private $_nestedResultMap;
+ private $_valueType;
+ private $_typeHandler;
+ private $_isLazyLoad=false;
+ private $_select;
+
+ private $_hostResultMapID='inplicit internal mapping';
+
+ const LIST_TYPE = 0;
+ const ARRAY_TYPE = 1;
+
+ /**
+ * Gets the containing result map ID.
+ * @param TResultMap containing result map.
+ */
+ public function __construct($resultMap=null)
+ {
+ if($resultMap instanceof TResultMap)
+ $this->_hostResultMapID = $resultMap->getID();
+ }
+
+ /**
+ * @return mixed null value replacement.
+ */
+ public function getNullValue()
+ {
+ return $this->_nullValue;
+ }
+
+ /**
+ * @param mixed null value replacement.
+ */
+ public function setNullValue($value)
+ {
+ $this->_nullValue = $value;
+ }
+
+ /**
+ * @return string name of a property of the result object that will be set to.
+ */
+ public function getProperty()
+ {
+ return $this->_propertyName;
+ }
+
+ /**
+ * @param string name of a property of the result object that will be set to.
+ */
+ public function setProperty($value)
+ {
+ $this->_propertyName = $value;
+ }
+
+ /**
+ * @return string name of the column in the result set from which the value
+ * will be used to populate the property.
+ */
+ public function getColumn()
+ {
+ return $this->_columnName;
+ }
+
+ /**
+ * @param string name of the column in the result set from which the value
+ * will be used to populate the property.
+ */
+ public function setColumn($value)
+ {
+ $this->_columnName = $value;
+ }
+
+ /**
+ * @return int index of the column in the ResultSet from which the value will
+ * be used to populate the object property
+ */
+ public function getColumnIndex()
+ {
+ return $this->_columnIndex;
+ }
+
+ /**
+ * @param int index of the column in the ResultSet from which the value will
+ * be used to populate the object property
+ */
+ public function setColumnIndex($value)
+ {
+ $this->_columnIndex = TPropertyValue::ensureInteger($value);
+ }
+
+ /**
+ * @return string ID of another <resultMap> used to fill the property.
+ */
+ public function getResultMapping()
+ {
+ return $this->_nestedResultMapName;
+ }
+
+ /**
+ * @param string ID of another <resultMap> used to fill the property.
+ */
+ public function setResultMapping($value)
+ {
+ $this->_nestedResultMapName = $value;
+ }
+
+ /**
+ * @return TResultMap nested result map.
+ */
+ public function getNestedResultMap()
+ {
+ return $this->_nestedResultMap;
+ }
+
+ /**
+ * @param TResult nested result map.
+ */
+ public function setNestedResultMap($value)
+ {
+ $this->_nestedResultMap = $value;
+ }
+
+ /**
+ * @return string property type of the object property to be set.
+ */
+ public function getType()
+ {
+ return $this->_valueType;
+ }
+
+ /**
+ * @param string property type of the object property to be set.
+ */
+ public function setType($value)
+ {
+ $this->_valueType = $value;
+ }
+
+ /**
+ * @return string custom type handler class name (may use namespace).
+ */
+ public function getTypeHandler()
+ {
+ return $this->_typeHandler;
+ }
+
+ /**
+ * @param string custom type handler class name (may use namespace).
+ */
+ public function setTypeHandler($value)
+ {
+ $this->_typeHandler = $value;
+ }
+
+ /**
+ * @return string name of another mapped statement
+ */
+ public function getSelect()
+ {
+ return $this->_select;
+ }
+
+ /**
+ * The select property is used to describe a relationship between objects
+ * and to automatically load complex (i.e. user defined) property types.
+ * @param string name of another mapped statement.
+ */
+ public function setSelect($value)
+ {
+ $this->_select = $value;
+ }
+
+ /**
+ * @return boolean indicate whether or not the select statement's results should be lazy loaded
+ */
+ public function getLazyLoad()
+ {
+ return $this->_isLazyLoad;
+ }
+
+ /**
+ * @param boolean indicate whether or not the select statement's results should be lazy loaded
+ */
+ public function setLazyLoad($value)
+ {
+ $this->_isLazyLoad = TPropertyValue::ensureBoolean($value,false);
+ }
+
+ /**
+ * Gets the value for the current property, converts to applicable type if necessary.
+ * @param TSqlMapTypeHandlerRegistry type handler registry
+ * @param array result row
+ * @return mixed property value.
+ */
+ public function getPropertyValue($registry,$row)
+ {
+ $value = null;
+ $index = $this->getColumnIndex();
+ $name = $this->getColumn();
+ if($index > 0 && isset($row[$index]))
+ $value = $this->getTypedValue($registry,$row[$index]);
+ else if(isset($row[$name]))
+ $value = $this->getTypedValue($registry,$row[$name]);
+ if(($value===null) && ($this->getNullValue()!==null))
+ $value = $this->getTypedValue($registry,$this->getNullValue());
+ return $value;
+ }
+
+ /**
+ * @param TSqlMapTypeHandlerRegistry type handler registry
+ * @param mixed raw property value
+ * @return mixed property value casted to specific type.
+ */
+ protected function getTypedValue($registry,$value)
+ {
+ if(($handler = $this->createTypeHandler($registry))!==null)
+ return $handler->getResult($value);
+ else
+ return $registry->convertToType($this->getType(), $value);
+ }
+
+ /**
+ * Create type handler from {@link Type setType()} or {@link TypeHandler setTypeHandler}.
+ * @param TSqlMapTypeHandlerRegistry type handler registry
+ * @return TSqlMapTypeHandler type handler.
+ */
+ protected function createTypeHandler($registry)
+ {
+ $type=$this->getTypeHandler() ? $this->getTypeHandler() : $this->getType();
+ $handler=$registry->getTypeHandler($type);
+ if($handler===null && $this->getTypeHandler())
+ $handler = Prado::createComponent($type);
+ return $handler;
+ }
+
+ /**
+ * Determines if the type is an instance of ArrayAccess, TList or an array.
+ * @return int TResultProperty::LIST_TYPE or TResultProperty::ARRAY_TYPE
+ */
+ protected function getPropertyValueType()
+ {
+ if(class_exists($type = $this->getType(), false)) //NO force autoloading
+ {
+ if($type==='TList')
+ return self::LIST_TYPE;
+ $class = new ReflectionClass($type);
+ if($class->isSubclassOf('TList'))
+ return self::LIST_TYPE;
+ if($class->implementsInterface('ArrayAccess'))
+ return self::ARRAY_TYPE;
+ }
+ if(strtolower($type) == 'array')
+ return self::ARRAY_TYPE;
+ }
+
+ /**
+ * Returns true if the result property {@link Type getType()} is of TList type
+ * or that the actual result object is an instance of TList.
+ * @param object result object
+ * @return boolean true if the result object is an instance of TList
+ */
+ public function instanceOfListType($target)
+ {
+ if($this->getType()===null)
+ return TPropertyAccess::get($target,$this->getProperty()) instanceof TList;
+ return $this->getPropertyValueType() == self::LIST_TYPE;
+ }
+
+ /**
+ * Returns true if the result property {@link Type getType()} is of ArrayAccess
+ * or that the actual result object is an array or implements ArrayAccess
+ * @param object result object
+ * @return boolean true if the result object is an instance of ArrayAccess or is an array.
+ */
+ public function instanceOfArrayType($target)
+ {
+ if($this->getType()===null)
+ {
+ $prop = TPropertyAccess::get($target,$this->getProperty());
+ if(is_object($prop))
+ return $prop instanceof ArrayAccess;
+ return is_array($prop);
+ }
+ return $this->getPropertyValueType() == self::ARRAY_TYPE;
+ }
+
+ public function __sleep()
+ {
+ $exprops = array(); $cn = 'TResultProperty';
+ if ($this->_nullValue===null) $exprops[] = "\0$cn\0_nullValue";
+ if ($this->_propertyName===null) $exprops[] = "\0$cn\0_propertyNama";
+ if ($this->_columnName===null) $exprops[] = "\0$cn\0_columnName";
+ if ($this->_columnIndex==-1) $exprops[] = "\0$cn\0_columnIndex";
+ if ($this->_nestedResultMapName===null) $exprops[] = "\0$cn\0_nestedResultMapName";
+ if ($this->_nestedResultMap===null) $exprops[] = "\0$cn\0_nestedResultMap";
+ if ($this->_valueType===null) $exprops[] = "\0$cn\0_valueType";
+ if ($this->_typeHandler===null) $exprops[] = "\0$cn\0_typeHandler";
+ if ($this->_isLazyLoad===false) $exprops[] = "\0$cn\0_isLazyLoad";
+ if ($this->_select===null) $exprops[] = "\0$cn\0_select";
+ return array_diff(parent::__sleep(),$exprops);
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TSimpleDynamicParser.php b/lib/prado/framework/Data/SqlMap/Configuration/TSimpleDynamicParser.php
new file mode 100644
index 0000000..41b706a
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TSimpleDynamicParser.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * TSimpleDynamicParser 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.SqlMap.Configuration
+ */
+
+/**
+ * TSimpleDynamicParser finds place holders $name$ in the sql text and replaces
+ * it with a TSimpleDynamicParser::DYNAMIC_TOKEN.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSimpleDynamicParser
+{
+ const PARAMETER_TOKEN_REGEXP = '/\$([^\$]+)\$/';
+ const DYNAMIC_TOKEN = '`!`';
+
+ /**
+ * Parse the sql text for dynamic place holders of the form $name$.
+ * @param string Sql text.
+ * @return array name value pairs 'sql' and 'parameters'.
+ */
+ public function parse($sqlText)
+ {
+ $matches = array();
+ $mappings = array();
+ preg_match_all(self::PARAMETER_TOKEN_REGEXP, $sqlText, $matches);
+ for($i = 0, $k=count($matches[1]); $i<$k; $i++)
+ {
+ $mappings[] = $matches[1][$i];
+ $sqlText = str_replace($matches[0][$i], self::DYNAMIC_TOKEN, $sqlText);
+ }
+ return array('sql'=>$sqlText, 'parameters'=>$mappings);
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapCacheModel.php b/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapCacheModel.php
new file mode 100644
index 0000000..8a1e344
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapCacheModel.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * TSqlMapCacheModel, TSqlMapCacheTypes and TSqlMapCacheKey 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.SqlMap.Configuration
+ */
+
+/**
+ * TSqlMapCacheModel corresponds to the <cacheModel> sql mapping configuration tag.
+ *
+ * The results from a query Mapped Statement can be cached simply by specifying
+ * the {@link CacheModel TSqlMapStatement::setCacheModel()} property in <statement> tag.
+ * A cache model is a configured cache that is defined within the sql map
+ * configuration file. Cache models are configured using the <cacheModel> element.
+ *
+ * The cache model uses a pluggable framework for supporting different types of
+ * caches. The choice of cache is specified by the {@link Implementation setImplementation()}
+ * property. The class name specified must be one of {@link TSqlMapCacheTypes}.
+ *
+ * The cache implementations, LRU and FIFO cache below do not persist across
+ * requests. That is, once the request is complete, all cache data is lost.
+ * These caches are useful queries that results in the same repeated data during
+ * the current request.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapCacheModel extends TComponent
+{
+ private $_cache;
+ private $_hits = 0;
+ private $_requests = 0;
+ private $_id;
+ private $_implementation=TSqlMapCacheTypes::Basic;
+ private $_properties = array();
+ private $_flushInterval = 0;
+
+ private static $_cacheTypes = array();
+
+ public static function registerCacheType($type, $className)
+ {
+ self::$_cacheTypes[$type] = $className;
+ }
+
+ /**
+ * @return string unique cache model identifier.
+ */
+ public function getID()
+ {
+ return $this->_id;
+ }
+
+ /**
+ * @param string unique cache model identifier.
+ */
+ public function setID($value)
+ {
+ $this->_id = $value;
+ }
+
+ /**
+ * @return string cache implements of TSqlMapCacheTypes, either 'Basic', 'LRU' or 'FIFO'.
+ */
+ public function getImplementation()
+ {
+ return $this->_implementation;
+ }
+
+ /**
+ * @param string cache implements of TSqlMapCacheTypes, either 'Basic', 'LRU' or 'FIFO'.
+ */
+ public function setImplementation($value)
+ {
+ if (isset(self::$_cacheTypes[$value]))
+ $this->_implementation = $value;
+ else
+ $this->_implementation = TPropertyValue::ensureEnum($value,'TSqlMapCacheTypes');
+ }
+
+ /**
+ * @param integer the number of seconds in which the cached value will expire. 0 means never expire.
+ */
+ public function setFlushInterval($value)
+ {
+ $this->_flushInterval=TPropertyValue::ensureInteger($value);
+ }
+
+ /**
+ * @return integer cache duration.
+ */
+ public function getFlushInterval()
+ {
+ return $this->_flushInterval;
+ }
+
+ /**
+ * Initialize the cache implementation, sets the actual cache contain if supplied.
+ * @param ISqLMapCache cache implementation instance.
+ */
+ public function initialize($cache=null)
+ {
+ if($cache===null)
+ $this->_cache= Prado::createComponent($this->getImplementationClass(), $this);
+ else
+ $this->_cache=$cache;
+ }
+
+ /**
+ * @return string cache implementation class name.
+ */
+ public function getImplementationClass()
+ {
+ $implementation = $this->_implementation;
+ if (isset(self::$_cacheTypes[$implementation])) return self::$_cacheTypes[$implementation];
+
+ switch(TPropertyValue::ensureEnum($implementation,'TSqlMapCacheTypes'))
+ {
+ case TSqlMapCacheTypes::FIFO: return 'TSqlMapFifoCache';
+ case TSqlMapCacheTypes::LRU : return 'TSqlMapLruCache';
+ case TSqlMapCacheTypes::Basic : return 'TSqlMapApplicationCache';
+ }
+ }
+
+ /**
+ * Register a mapped statement that will trigger a cache flush.
+ * @param TMappedStatement mapped statement that may flush the cache.
+ */
+ public function registerTriggerStatement($mappedStatement)
+ {
+ $mappedStatement->attachEventHandler('OnExecuteQuery',array($this, 'flush'));
+ }
+
+ /**
+ * Clears the cache.
+ */
+ public function flush()
+ {
+ $this->_cache->flush();
+ }
+
+ /**
+ * @param TSqlMapCacheKey|string cache key
+ * @return mixed cached value.
+ */
+ public function get($key)
+ {
+ if($key instanceof TSqlMapCacheKey)
+ $key = $key->getHash();
+
+ //if flush ?
+ $value = $this->_cache->get($key);
+ $this->_requests++;
+ if($value!==null)
+ $this->_hits++;
+ return $value;
+ }
+
+ /**
+ * @param TSqlMapCacheKey|string cache key
+ * @param mixed value to be cached.
+ */
+ public function set($key, $value)
+ {
+ if($key instanceof TSqlMapCacheKey)
+ $key = $key->getHash();
+
+ if($value!==null)
+ $this->_cache->set($key, $value, $this->_flushInterval);
+ }
+
+ /**
+ * @return float cache hit ratio.
+ */
+ public function getHitRatio()
+ {
+ if($this->_requests != 0)
+ return $this->_hits / $this->_requests;
+ else
+ return 0;
+ }
+}
+
+/**
+ * TSqlMapCacheTypes enumerable class.
+ *
+ * Implemented cache are 'Basic', 'FIFO' and 'LRU'.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapCacheTypes extends TEnumerable
+{
+ const Basic='Basic';
+ const FIFO='FIFO';
+ const LRU='LRU';
+}
+
+/**
+ * TSqlMapCacheKey class.
+ *
+ * Provides a hash of the object to be cached.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapCacheKey
+{
+ private $_key;
+
+ /**
+ * @param mixed object to be cached.
+ */
+ public function __construct($object)
+ {
+ $this->_key = $this->generateKey(serialize($object));
+ }
+
+ /**
+ * @param string serialized object
+ * @return string crc32 hash of the serialized object.
+ */
+ protected function generateKey($string)
+ {
+ return sprintf('%x',crc32($string));
+ }
+
+ /**
+ * @return string object hash.
+ */
+ public function getHash()
+ {
+ return $this->_key;
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapStatement.php b/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapStatement.php
new file mode 100644
index 0000000..abb21c7
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapStatement.php
@@ -0,0 +1,444 @@
+<?php
+/**
+ * TSqlMapStatement, TSqlMapInsert, TSqlMapUpdate, TSqlMapDelete,
+ * TSqlMapSelect and TSqlMapSelectKey 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.SqlMap.Configuration
+ */
+
+/**
+ * TSqlMapStatement class corresponds to <statement> element.
+ *
+ * Mapped Statements can hold any SQL statement and can use Parameter Maps
+ * and Result Maps for input and output.
+ *
+ * The <statement> element is a general "catch all" element that can be used
+ * for any type of SQL statement. Generally it is a good idea to use one of the
+ * more specific statement-type elements. The more specific elements provided
+ * better error-checking and even more functionality. (For example, the insert
+ * statement can return a database-generated key.)
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapStatement extends TComponent
+{
+ private $_parameterMapName;
+ private $_parameterMap;
+ private $_parameterClassName;
+ private $_resultMapName;
+ private $_resultMap;
+ private $_resultClassName;
+ private $_cacheModelName;
+ private $_SQL;
+ private $_listClass;
+ private $_typeHandler;
+ private $_extendStatement;
+ private $_cache;
+ private $_ID;
+
+ /**
+ * @return string name for this statement, unique to each sql map manager.
+ */
+ public function getID()
+ {
+ return $this->_ID;
+ }
+
+ /**
+ * @param string name for this statement, which must be unique for each sql map manager.
+ */
+ public function setID($value)
+ {
+ $this->_ID=$value;
+ }
+
+ /**
+ * @return string name of a parameter map.
+ */
+ public function getParameterMap()
+ {
+ return $this->_parameterMapName;
+ }
+
+ /**
+ * A Parameter Map defines an ordered list of values that match up with
+ * the "?" placeholders of a standard, parameterized query statement.
+ * @param string parameter map name.
+ */
+ public function setParameterMap($value)
+ {
+ $this->_parameterMapName = $value;
+ }
+
+ /**
+ * @return string parameter class name.
+ */
+ public function getParameterClass()
+ {
+ return $this->_parameterClassName;
+ }
+
+ /**
+ * If a {@link ParameterMap setParameterMap()} property is not specified,
+ * you may specify a ParameterClass instead and use inline parameters.
+ * The value of the parameterClass attribute can be any existing PHP class name.
+ * @param string parameter class name.
+ */
+ public function setParameterClass($value)
+ {
+ $this->_parameterClassName = $value;
+ }
+
+ /**
+ * @return string result map name.
+ */
+ public function getResultMap()
+ {
+ return $this->_resultMapName;
+ }
+
+ /**
+ * A Result Map lets you control how data is extracted from the result of a
+ * query, and how the columns are mapped to object properties.
+ * @param string result map name.
+ */
+ public function setResultMap($value)
+ {
+ $this->_resultMapName = $value;
+ }
+
+ /**
+ * @return string result class name.
+ */
+ public function getResultClass()
+ {
+ return $this->_resultClassName;
+ }
+
+ /**
+ * If a {@link ResultMap setResultMap()} is not specified, you may specify a
+ * ResultClass instead. The value of the ResultClass property can be the
+ * name of a PHP class or primitives like integer, string, or array. The
+ * class specified will be automatically mapped to the columns in the
+ * result, based on the result metadata.
+ * @param string result class name.
+ */
+ public function setResultClass($value)
+ {
+ $this->_resultClassName = $value;
+ }
+
+ /**
+ * @return string cache mode name.
+ */
+ public function getCacheModel()
+ {
+ return $this->_cacheModelName;
+ }
+
+ /**
+ * @param string cache mode name.
+ */
+ public function setCacheModel($value)
+ {
+ $this->_cacheModelName = $value;
+ }
+
+ /**
+ * @return TSqlMapCacheModel cache implementation instance for this statement.
+ */
+ public function getCache()
+ {
+ return $this->_cache;
+ }
+
+ /**
+ * @param TSqlMapCacheModel cache implementation instance for this statement.
+ */
+ public function setCache($value)
+ {
+ $this->_cache = $value;
+ }
+
+ /**
+ * @return TStaticSql sql text container.
+ */
+ public function getSqlText()
+ {
+ return $this->_SQL;
+ }
+
+ /**
+ * @param TStaticSql sql text container.
+ */
+ public function setSqlText($value)
+ {
+ $this->_SQL = $value;
+ }
+
+ /**
+ * @return string name of a PHP class that implements ArrayAccess.
+ */
+ public function getListClass()
+ {
+ return $this->_listClass;
+ }
+
+ /**
+ * An ArrayAccess class can be specified to handle the type of objects in the collection.
+ * @param string name of a PHP class that implements ArrayAccess.
+ */
+ public function setListClass($value)
+ {
+ $this->_listClass = $value;
+ }
+
+ /**
+ * @return string another statement element name.
+ */
+ public function getExtends()
+ {
+ return $this->_extendStatement;
+ }
+
+ /**
+ * @param string name of another statement element to extend.
+ */
+ public function setExtends($value)
+ {
+ $this->_extendStatement = $value;
+ }
+
+ /**
+ * @return TResultMap the result map corresponding to the
+ * {@link ResultMap getResultMap()} property.
+ */
+ public function resultMap()
+ {
+ return $this->_resultMap;
+ }
+
+ /**
+ * @return TParameterMap the parameter map corresponding to the
+ * {@link ParameterMap getParameterMap()} property.
+ */
+ public function parameterMap()
+ {
+ return $this->_parameterMap;
+ }
+
+ /**
+ * @param TInlineParameterMap parameter extracted from the sql text.
+ */
+ public function setInlineParameterMap($map)
+ {
+ $this->_parameterMap = $map;
+ }
+
+ /**
+ * @param TSqlMapManager initialize the statement, sets the result and parameter maps.
+ */
+ public function initialize($manager)
+ {
+ if(strlen($this->_resultMapName) > 0)
+ $this->_resultMap = $manager->getResultMap($this->_resultMapName);
+ if(strlen($this->_parameterMapName) > 0)
+ $this->_parameterMap = $manager->getParameterMap($this->_parameterMapName);
+ }
+
+ /**
+ * @param TSqlMapTypeHandlerRegistry type handler registry
+ * @return ArrayAccess new instance of list class.
+ */
+ public function createInstanceOfListClass($registry)
+ {
+ if(strlen($type = $this->getListClass()) > 0)
+ return $this->createInstanceOf($registry,$type);
+ return array();
+ }
+
+ /**
+ * Create a new instance of a given type.
+ * @param TSqlMapTypeHandlerRegistry type handler registry
+ * @param string result class name.
+ * @param array result data.
+ * @return mixed result object.
+ */
+ protected function createInstanceOf($registry,$type,$row=null)
+ {
+ $handler = $registry->getTypeHandler($type);
+ if($handler!==null)
+ return $handler->createNewInstance($row);
+ else
+ return $registry->createInstanceOf($type);
+ }
+
+ /**
+ * Create a new instance of result class.
+ * @param TSqlMapTypeHandlerRegistry type handler registry
+ * @param array result data.
+ * @return mixed result object.
+ */
+ public function createInstanceOfResultClass($registry,$row)
+ {
+ if(strlen($type= $this->getResultClass()) > 0)
+ return $this->createInstanceOf($registry,$type,$row);
+ }
+
+ public function __sleep()
+ {
+ $cn = __CLASS__;
+ $exprops = array("\0$cn\0_resultMap");
+ if (!$this->_parameterMapName) $exprops[] = "\0$cn\0_parameterMapName";
+ if (!$this->_parameterMap) $exprops[] = "\0$cn\0_parameterMap";
+ if (!$this->_parameterClassName) $exprops[] = "\0$cn\0_parameterClassName";
+ if (!$this->_resultMapName) $exprops[] = "\0$cn\0_resultMapName";
+ if (!$this->_resultMap) $exprops[] = "\0$cn\0_resultMap";
+ if (!$this->_resultClassName) $exprops[] = "\0$cn\0_resultClassName";
+ if (!$this->_cacheModelName) $exprops[] = "\0$cn\0_cacheModelName";
+ if (!$this->_SQL) $exprops[] = "\0$cn\0_SQL";
+ if (!$this->_listClass) $exprops[] = "\0$cn\0_listClass";
+ if (!$this->_typeHandler) $exprops[] = "\0$cn\0_typeHandler";
+ if (!$this->_extendStatement) $exprops[] = "\0$cn\0_extendStatement";
+ if (!$this->_cache) $exprops[] = "\0$cn\0_cache";
+
+ return array_diff(parent::__sleep(),$exprops);
+ }
+
+}
+
+/**
+ * TSqlMapSelect class file.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TSqlMapSelect extends TSqlMapStatement
+{
+ private $_generate;
+
+ public function getGenerate(){ return $this->_generate; }
+ public function setGenerate($value){ $this->_generate = $value; }
+}
+
+/**
+ * TSqlMapInsert class corresponds to the <insert> element.
+ *
+ * The <insert> element allows <selectKey> child elements that can be used
+ * to generate a key to be used for the insert command.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapInsert extends TSqlMapStatement
+{
+ private $_selectKey=null;
+
+ /**
+ * @return TSqlMapSelectKey select key element.
+ */
+ public function getSelectKey()
+ {
+ return $this->_selectKey;
+ }
+
+ /**
+ * @param TSqlMapSelectKey select key.
+ */
+ public function setSelectKey($value)
+ {
+ $this->_selectKey = $value;
+ }
+}
+
+/**
+ * TSqlMapUpdate class corresponds to <update> element.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapUpdate extends TSqlMapStatement
+{
+}
+
+/**
+ * TSqlMapDelete class corresponds to the <delete> element.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapDelete extends TSqlMapUpdate
+{
+}
+
+/**
+ * TSqlMapSelect corresponds to the <selectKey> element.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapSelectKey extends TSqlMapStatement
+{
+ private $_type = 'post';
+ private $_property;
+
+ /**
+ * @return string select generated key type, 'post' or 'pre'.
+ */
+ public function getType()
+ {
+ return $this->_type;
+ }
+
+ /**
+ * @param string select generated key type, 'post' or 'pre'.
+ */
+ public function setType($value)
+ {
+ $this->_type = strtolower($value) == 'post' ? 'post' : 'pre';
+ }
+
+ /**
+ * @return string property name for the generated key.
+ */
+ public function getProperty()
+ {
+ return $this->_property;
+ }
+
+ /**
+ * @param string property name for the generated key.
+ */
+ public function setProperty($value)
+ {
+ $this->_property = $value;
+ }
+
+ /**
+ * @throws TSqlMapConfigurationException extends is unsupported.
+ */
+ public function setExtends($value)
+ {
+ throw new TSqlMapConfigurationException('sqlmap_can_not_extend_select_key');
+ }
+
+ /**
+ * @return boolean true if key is generated after insert command, false otherwise.
+ */
+ public function getIsAfter()
+ {
+ return $this->_type == 'post';
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapXmlConfiguration.php b/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapXmlConfiguration.php
new file mode 100644
index 0000000..062b65e
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Configuration/TSqlMapXmlConfiguration.php
@@ -0,0 +1,801 @@
+<?php
+/**
+ * TSqlMapXmlConfigBuilder, TSqlMapXmlConfiguration, TSqlMapXmlMappingConfiguration 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.SqlMap.Configuration
+ */
+
+Prado::using('System.Data.SqlMap.Configuration.TSqlMapStatement');
+
+/**
+ * TSqlMapXmlConfig class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ */
+abstract class TSqlMapXmlConfigBuilder
+{
+ /**
+ * Create an instance of an object give by the attribute named 'class' in the
+ * node and set the properties on the object given by attribute names and values.
+ * @param SimpleXmlNode property node
+ * @return Object new instance of class with class name given by 'class' attribute value.
+ */
+ protected function createObjectFromNode($node)
+ {
+ if(isset($node['class']))
+ {
+ $obj = Prado::createComponent((string)$node['class']);
+ $this->setObjectPropFromNode($obj,$node,array('class'));
+ return $obj;
+ }
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_node_class_undef', $node, $this->getConfigFile());
+ }
+
+ /**
+ * For each attributes (excluding attribute named in $except) set the
+ * property of the $obj given by the name of the attribute with the value
+ * of the attribute.
+ * @param Object object instance
+ * @param SimpleXmlNode property node
+ * @param array exception property name
+ */
+ protected function setObjectPropFromNode($obj,$node,$except=array())
+ {
+ foreach($node->attributes() as $name=>$value)
+ {
+ if(!in_array($name,$except))
+ {
+ if($obj->canSetProperty($name))
+ $obj->{$name} = (string)$value;
+ else
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_invalid_property', $name, get_class($obj),
+ $node, $this->getConfigFile());
+ }
+ }
+ }
+
+ /**
+ * Gets the filename relative to the basefile.
+ * @param string base filename
+ * @param string relative filename
+ * @return string absolute filename.
+ */
+ protected function getAbsoluteFilePath($basefile,$resource)
+ {
+ $basedir = dirname($basefile);
+ $file = realpath($basedir.DIRECTORY_SEPARATOR.$resource);
+ if(!is_string($file) || !is_file($file))
+ $file = realpath($resource);
+ if(is_string($file) && is_file($file))
+ return $file;
+ else
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_unable_to_find_resource', $resource);
+ }
+
+ /**
+ * Load document using simple xml.
+ * @param string filename.
+ * @return SimpleXmlElement xml document.
+ */
+ protected function loadXmlDocument($filename,TSqlMapXmlConfiguration $config)
+ {
+ if( strpos($filename, '${') !== false)
+ $filename = $config->replaceProperties($filename);
+
+ if(!is_file($filename))
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_unable_to_find_config', $filename);
+ return simplexml_load_string($config->replaceProperties(file_get_contents($filename)));
+ }
+
+ /**
+ * Get element node by ID value (try for attribute name ID as case insensitive).
+ * @param SimpleXmlDocument $document
+ * @param string tag name.
+ * @param string id value.
+ * @return SimpleXmlElement node if found, null otherwise.
+ */
+ protected function getElementByIdValue($document, $tag, $value)
+ {
+ //hack to allow upper case and lower case attribute names.
+ foreach(array('id','ID','Id', 'iD') as $id)
+ {
+ $xpath = "//{$tag}[@{$id}='{$value}']";
+ foreach($document->xpath($xpath) as $node)
+ return $node;
+ }
+ }
+
+ /**
+ * @return string configuration file.
+ */
+ protected abstract function getConfigFile();
+}
+
+/**
+ * TSqlMapXmlConfig class.
+ *
+ * Configures the TSqlMapManager using xml configuration file.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapXmlConfiguration extends TSqlMapXmlConfigBuilder
+{
+ /**
+ * @var TSqlMapManager manager
+ */
+ private $_manager;
+ /**
+ * @var string configuration file.
+ */
+ private $_configFile;
+ /**
+ * @var array global properties.
+ */
+ private $_properties=array();
+
+ /**
+ * @param TSqlMapManager manager instance.
+ */
+ public function __construct($manager)
+ {
+ $this->_manager=$manager;
+ }
+
+ public function getManager()
+ {
+ return $this->_manager;
+ }
+
+ protected function getConfigFile()
+ {
+ return $this->_configFile;
+ }
+
+ /**
+ * Configure the TSqlMapManager using the given xml file.
+ * @param string SqlMap configuration xml file.
+ */
+ public function configure($filename=null)
+ {
+ $this->_configFile=$filename;
+ $document = $this->loadXmlDocument($filename,$this);
+
+ foreach($document->xpath('//property') as $property)
+ $this->loadGlobalProperty($property);
+
+ foreach($document->xpath('//typeHandler') as $handler)
+ $this->loadTypeHandler($handler);
+
+ foreach($document->xpath('//connection[last()]') as $conn)
+ $this->loadDatabaseConnection($conn);
+
+ //try to load configuration in the current config file.
+ $mapping = new TSqlMapXmlMappingConfiguration($this);
+ $mapping->configure($filename);
+
+ foreach($document->xpath('//sqlMap') as $sqlmap)
+ $this->loadSqlMappingFiles($sqlmap);
+
+ $this->resolveResultMapping();
+ $this->attachCacheModels();
+ }
+
+ /**
+ * Load global replacement property.
+ * @param SimpleXmlElement property node.
+ */
+ protected function loadGlobalProperty($node)
+ {
+ $this->_properties[(string)$node['name']] = (string)$node['value'];
+ }
+
+ /**
+ * Load the type handler configurations.
+ * @param SimpleXmlElement type handler node
+ */
+ protected function loadTypeHandler($node)
+ {
+ $handler = $this->createObjectFromNode($node);
+ $this->_manager->getTypeHandlers()->registerTypeHandler($handler);
+ }
+
+ /**
+ * Load the database connection tag.
+ * @param SimpleXmlElement connection node.
+ */
+ protected function loadDatabaseConnection($node)
+ {
+ $conn = $this->createObjectFromNode($node);
+ $this->_manager->setDbConnection($conn);
+ }
+
+ /**
+ * Load SqlMap mapping configuration.
+ * @param unknown_type $node
+ */
+ protected function loadSqlMappingFiles($node)
+ {
+ if(strlen($resource = (string)$node['resource']) > 0)
+ {
+ if( strpos($resource, '${') !== false)
+ $resource = $this->replaceProperties($resource);
+
+ $mapping = new TSqlMapXmlMappingConfiguration($this);
+ $filename = $this->getAbsoluteFilePath($this->_configFile, $resource);
+ $mapping->configure($filename);
+ }
+ }
+
+ /**
+ * Resolve nest result mappings.
+ */
+ protected function resolveResultMapping()
+ {
+ $maps = $this->_manager->getResultMaps();
+ foreach($maps as $entry)
+ {
+ foreach($entry->getColumns() as $item)
+ {
+ $resultMap = $item->getResultMapping();
+ if(strlen($resultMap) > 0)
+ {
+ if($maps->contains($resultMap))
+ $item->setNestedResultMap($maps[$resultMap]);
+ else
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_unable_to_find_result_mapping',
+ $resultMap, $this->_configFile, $entry->getID());
+ }
+ }
+ if($entry->getDiscriminator()!==null)
+ $entry->getDiscriminator()->initialize($this->_manager);
+ }
+ }
+
+ /**
+ * Set the cache for each statement having a cache model property.
+ */
+ protected function attachCacheModels()
+ {
+ foreach($this->_manager->getMappedStatements() as $mappedStatement)
+ {
+ if(strlen($model = $mappedStatement->getStatement()->getCacheModel()) > 0)
+ {
+ $cache = $this->_manager->getCacheModel($model);
+ $mappedStatement->getStatement()->setCache($cache);
+ }
+ }
+ }
+
+ /**
+ * Replace the place holders ${name} in text with properties the
+ * corresponding global property value.
+ * @param string original string.
+ * @return string string with global property replacement.
+ */
+ public function replaceProperties($string)
+ {
+ foreach($this->_properties as $find => $replace)
+ $string = str_replace('${'.$find.'}', $replace, $string);
+ return $string;
+ }
+}
+
+/**
+ * Loads the statements, result maps, parameters maps from xml configuration.
+ *
+ * description
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Configuration
+ * @since 3.1
+ */
+class TSqlMapXmlMappingConfiguration extends TSqlMapXmlConfigBuilder
+{
+ private $_xmlConfig;
+ private $_configFile;
+ private $_manager;
+
+ private $_document;
+
+ private $_FlushOnExecuteStatements=array();
+
+ /**
+ * Regular expressions for escaping simple/inline parameter symbols
+ */
+ const SIMPLE_MARK='$';
+ const INLINE_SYMBOL='#';
+ const ESCAPED_SIMPLE_MARK_REGEXP='/\$\$/';
+ const ESCAPED_INLINE_SYMBOL_REGEXP='/\#\#/';
+ const SIMPLE_PLACEHOLDER='`!!`';
+ const INLINE_PLACEHOLDER='`!!!`';
+
+ /**
+ * @param TSqlMapXmlConfiguration parent xml configuration.
+ */
+ public function __construct(TSqlMapXmlConfiguration $xmlConfig)
+ {
+ $this->_xmlConfig=$xmlConfig;
+ $this->_manager=$xmlConfig->getManager();
+ }
+
+ protected function getConfigFile()
+ {
+ return $this->_configFile;
+ }
+
+ /**
+ * Configure an XML mapping.
+ * @param string xml mapping filename.
+ */
+ public function configure($filename)
+ {
+ $this->_configFile=$filename;
+ $document = $this->loadXmlDocument($filename,$this->_xmlConfig);
+ $this->_document=$document;
+
+ static $bCacheDependencies;
+ if($bCacheDependencies === null)
+ $bCacheDependencies = Prado::getApplication()->getMode() !== TApplicationMode::Performance;
+
+ if($bCacheDependencies)
+ $this->_manager->getCacheDependencies()
+ ->getDependencies()
+ ->add(new TFileCacheDependency($filename));
+
+ foreach($document->xpath('//resultMap') as $node)
+ $this->loadResultMap($node);
+
+ foreach($document->xpath('//parameterMap') as $node)
+ $this->loadParameterMap($node);
+
+ foreach($document->xpath('//statement') as $node)
+ $this->loadStatementTag($node);
+
+ foreach($document->xpath('//select') as $node)
+ $this->loadSelectTag($node);
+
+ foreach($document->xpath('//insert') as $node)
+ $this->loadInsertTag($node);
+
+ foreach($document->xpath('//update') as $node)
+ $this->loadUpdateTag($node);
+
+ foreach($document->xpath('//delete') as $node)
+ $this->loadDeleteTag($node);
+
+ foreach($document->xpath('//procedure') as $node)
+ $this->loadProcedureTag($node);
+
+ foreach($document->xpath('//cacheModel') as $node)
+ $this->loadCacheModel($node);
+
+ $this->registerCacheTriggers();
+ }
+
+ /**
+ * Load the result maps.
+ * @param SimpleXmlElement result map node.
+ */
+ protected function loadResultMap($node)
+ {
+ $resultMap = $this->createResultMap($node);
+
+ //find extended result map.
+ if(strlen($extendMap = $resultMap->getExtends()) > 0)
+ {
+ if(!$this->_manager->getResultMaps()->contains($extendMap))
+ {
+ $extendNode=$this->getElementByIdValue($this->_document,'resultMap',$extendMap);
+ if($extendNode!==null)
+ $this->loadResultMap($extendNode);
+ }
+
+ if(!$this->_manager->getResultMaps()->contains($extendMap))
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_unable_to_find_parent_result_map', $node, $this->_configFile, $extendMap);
+
+ $superMap = $this->_manager->getResultMap($extendMap);
+ $resultMap->getColumns()->mergeWith($superMap->getColumns());
+ }
+
+ //add the result map
+ if(!$this->_manager->getResultMaps()->contains($resultMap->getID()))
+ $this->_manager->addResultMap($resultMap);
+ }
+
+ /**
+ * Create a new result map and its associated result properties,
+ * disciminiator and sub maps.
+ * @param SimpleXmlElement result map node
+ * @return TResultMap SqlMap result mapping.
+ */
+ protected function createResultMap($node)
+ {
+ $resultMap = new TResultMap();
+ $this->setObjectPropFromNode($resultMap,$node);
+
+ //result nodes
+ foreach($node->result as $result)
+ {
+ $property = new TResultProperty($resultMap);
+ $this->setObjectPropFromNode($property,$result);
+ $resultMap->addResultProperty($property);
+ }
+
+ //create the discriminator
+ $discriminator = null;
+ if(isset($node->discriminator))
+ {
+ $discriminator = new TDiscriminator();
+ $this->setObjectPropFromNode($discriminator, $node->discriminator);
+ $discriminator->initMapping($resultMap);
+ }
+
+ foreach($node->xpath('subMap') as $subMapNode)
+ {
+ if($discriminator===null)
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_undefined_discriminator', $node, $this->_configFile,$subMapNode);
+ $subMap = new TSubMap;
+ $this->setObjectPropFromNode($subMap,$subMapNode);
+ $discriminator->addSubMap($subMap);
+ }
+
+ if($discriminator!==null)
+ $resultMap->setDiscriminator($discriminator);
+
+ return $resultMap;
+ }
+
+ /**
+ * Load parameter map from xml.
+ *
+ * @param SimpleXmlElement parameter map node.
+ */
+ protected function loadParameterMap($node)
+ {
+ $parameterMap = $this->createParameterMap($node);
+
+ if(strlen($extendMap = $parameterMap->getExtends()) > 0)
+ {
+ if(!$this->_manager->getParameterMaps()->contains($extendMap))
+ {
+ $extendNode=$this->getElementByIdValue($this->_document,'parameterMap',$extendMap);
+ if($extendNode!==null)
+ $this->loadParameterMap($extendNode);
+ }
+
+ if(!$this->_manager->getParameterMaps()->contains($extendMap))
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_unable_to_find_parent_parameter_map', $node, $this->_configFile,$extendMap);
+ $superMap = $this->_manager->getParameterMap($extendMap);
+ $index = 0;
+ foreach($superMap->getPropertyNames() as $propertyName)
+ $parameterMap->insertProperty($index++,$superMap->getProperty($propertyName));
+ }
+ $this->_manager->addParameterMap($parameterMap);
+ }
+
+ /**
+ * Create a new parameter map from xml node.
+ * @param SimpleXmlElement parameter map node.
+ * @return TParameterMap new parameter mapping.
+ */
+ protected function createParameterMap($node)
+ {
+ $parameterMap = new TParameterMap();
+ $this->setObjectPropFromNode($parameterMap,$node);
+ foreach($node->parameter as $parameter)
+ {
+ $property = new TParameterProperty();
+ $this->setObjectPropFromNode($property,$parameter);
+ $parameterMap->addProperty($property);
+ }
+ return $parameterMap;
+ }
+
+ /**
+ * Load statement mapping from xml configuration file.
+ * @param SimpleXmlElement statement node.
+ */
+ protected function loadStatementTag($node)
+ {
+ $statement = new TSqlMapStatement();
+ $this->setObjectPropFromNode($statement,$node);
+ $this->processSqlStatement($statement, $node);
+ $mappedStatement = new TMappedStatement($this->_manager, $statement);
+ $this->_manager->addMappedStatement($mappedStatement);
+ }
+
+ /**
+ * Load extended SQL statements if application. Replaces global properties
+ * in the sql text. Extracts inline parameter maps.
+ * @param TSqlMapStatement mapped statement.
+ * @param SimpleXmlElement statement node.
+ */
+ protected function processSqlStatement($statement, $node)
+ {
+ $commandText = (string)$node;
+ if(strlen($extend = $statement->getExtends()) > 0)
+ {
+ $superNode = $this->getElementByIdValue($this->_document,'*',$extend);
+ if($superNode!==null)
+ $commandText = (string)$superNode . $commandText;
+ else
+ throw new TSqlMapConfigurationException(
+ 'sqlmap_unable_to_find_parent_sql', $extend, $this->_configFile,$node);
+ }
+ //$commandText = $this->_xmlConfig->replaceProperties($commandText);
+ $statement->initialize($this->_manager);
+ $this->applyInlineParameterMap($statement, $commandText, $node);
+ }
+
+ /**
+ * Extract inline parameter maps.
+ * @param TSqlMapStatement statement object.
+ * @param string sql text
+ * @param SimpleXmlElement statement node.
+ */
+ protected function applyInlineParameterMap($statement, $sqlStatement, $node)
+ {
+ $scope['file'] = $this->_configFile;
+ $scope['node'] = $node;
+
+ $sqlStatement=preg_replace(self::ESCAPED_INLINE_SYMBOL_REGEXP,self::INLINE_PLACEHOLDER,$sqlStatement);
+ if($statement->parameterMap() === null)
+ {
+ // Build a Parametermap with the inline parameters.
+ // if they exist. Then delete inline infos from sqltext.
+ $parameterParser = new TInlineParameterMapParser;
+ $sqlText = $parameterParser->parse($sqlStatement, $scope);
+ if(count($sqlText['parameters']) > 0)
+ {
+ $map = new TParameterMap();
+ $map->setID($statement->getID().'-InLineParameterMap');
+ $statement->setInlineParameterMap($map);
+ foreach($sqlText['parameters'] as $property)
+ $map->addProperty($property);
+ }
+ $sqlStatement = $sqlText['sql'];
+ }
+ $sqlStatement=preg_replace('/'.self::INLINE_PLACEHOLDER.'/',self::INLINE_SYMBOL,$sqlStatement);
+
+ $this->prepareSql($statement, $sqlStatement, $node);
+ }
+
+ /**
+ * Prepare the sql text (may extend to dynamic sql).
+ * @param TSqlMapStatement mapped statement.
+ * @param string sql text.
+ * @param SimpleXmlElement statement node.
+ * @todo Extend to dynamic sql.
+ */
+ protected function prepareSql($statement,$sqlStatement, $node)
+ {
+ $simpleDynamic = new TSimpleDynamicParser;
+ $sqlStatement=preg_replace(self::ESCAPED_SIMPLE_MARK_REGEXP,self::SIMPLE_PLACEHOLDER,$sqlStatement);
+ $dynamics = $simpleDynamic->parse($sqlStatement);
+ if(count($dynamics['parameters']) > 0)
+ {
+ $sql = new TSimpleDynamicSql($dynamics['parameters']);
+ $sqlStatement = $dynamics['sql'];
+ }
+ else
+ $sql = new TStaticSql();
+ $sqlStatement=preg_replace('/'.self::SIMPLE_PLACEHOLDER.'/',self::SIMPLE_MARK,$sqlStatement);
+ $sql->buildPreparedStatement($statement, $sqlStatement);
+ $statement->setSqlText($sql);
+ }
+
+ /**
+ * Load select statement from xml mapping.
+ * @param SimpleXmlElement select node.
+ */
+ protected function loadSelectTag($node)
+ {
+ $select = new TSqlMapSelect;
+ $this->setObjectPropFromNode($select,$node);
+ $this->processSqlStatement($select,$node);
+ $mappedStatement = new TMappedStatement($this->_manager, $select);
+ if(strlen($select->getCacheModel()) > 0)
+ $mappedStatement = new TCachingStatement($mappedStatement);
+
+ $this->_manager->addMappedStatement($mappedStatement);
+ }
+
+ /**
+ * Load insert statement from xml mapping.
+ * @param SimpleXmlElement insert node.
+ */
+ protected function loadInsertTag($node)
+ {
+ $insert = $this->createInsertStatement($node);
+ $this->processSqlStatement($insert, $node);
+ $mappedStatement = new TInsertMappedStatement($this->_manager, $insert);
+ $this->_manager->addMappedStatement($mappedStatement);
+ }
+
+ /**
+ * Create new insert statement from xml node.
+ * @param SimpleXmlElement insert node.
+ * @return TSqlMapInsert insert statement.
+ */
+ protected function createInsertStatement($node)
+ {
+ $insert = new TSqlMapInsert;
+ $this->setObjectPropFromNode($insert,$node);
+ if(isset($node->selectKey))
+ $this->loadSelectKeyTag($insert,$node->selectKey);
+ return $insert;
+ }
+
+ /**
+ * Load the selectKey statement from xml mapping.
+ * @param SimpleXmlElement selectkey node
+ */
+ protected function loadSelectKeyTag($insert, $node)
+ {
+ $selectKey = new TSqlMapSelectKey;
+ $this->setObjectPropFromNode($selectKey,$node);
+ $selectKey->setID($insert->getID());
+ $selectKey->setID($insert->getID().'.SelectKey');
+ $this->processSqlStatement($selectKey,$node);
+ $insert->setSelectKey($selectKey);
+ $mappedStatement = new TMappedStatement($this->_manager, $selectKey);
+ $this->_manager->addMappedStatement($mappedStatement);
+ }
+
+ /**
+ * Load update statement from xml mapping.
+ * @param SimpleXmlElement update node.
+ */
+ protected function loadUpdateTag($node)
+ {
+ $update = new TSqlMapUpdate;
+ $this->setObjectPropFromNode($update,$node);
+ $this->processSqlStatement($update, $node);
+ $mappedStatement = new TUpdateMappedStatement($this->_manager, $update);
+ $this->_manager->addMappedStatement($mappedStatement);
+ }
+
+ /**
+ * Load delete statement from xml mapping.
+ * @param SimpleXmlElement delete node.
+ */
+ protected function loadDeleteTag($node)
+ {
+ $delete = new TSqlMapDelete;
+ $this->setObjectPropFromNode($delete,$node);
+ $this->processSqlStatement($delete, $node);
+ $mappedStatement = new TDeleteMappedStatement($this->_manager, $delete);
+ $this->_manager->addMappedStatement($mappedStatement);
+ }
+
+ /**
+ * Load procedure statement from xml mapping.
+ * @todo Implement loading procedure
+ * @param SimpleXmlElement procedure node
+ */
+ protected function loadProcedureTag($node)
+ {
+ //var_dump('todo: add load procedure');
+ }
+
+ /**
+ * Load cache models from xml mapping.
+ * @param SimpleXmlElement cache node.
+ */
+ protected function loadCacheModel($node)
+ {
+ $cacheModel = new TSqlMapCacheModel;
+ $properties = array('id','implementation');
+ foreach($node->attributes() as $name=>$value)
+ {
+ if(in_array(strtolower($name), $properties))
+ $cacheModel->{'set'.$name}((string)$value);
+ }
+ $cache = Prado::createComponent($cacheModel->getImplementationClass(), $cacheModel);
+ $this->setObjectPropFromNode($cache,$node,$properties);
+
+ foreach($node->xpath('property') as $propertyNode)
+ {
+ $name = $propertyNode->attributes()->name;
+ if($name===null || $name==='') continue;
+
+ $value = $propertyNode->attributes()->value;
+ if($value===null || $value==='') continue;
+
+ if( !TPropertyAccess::has($cache, $name) ) continue;
+
+ TPropertyAccess::set($cache, $name, $value);
+ }
+
+ $this->loadFlushInterval($cacheModel,$node);
+
+ $cacheModel->initialize($cache);
+ $this->_manager->addCacheModel($cacheModel);
+ foreach($node->xpath('flushOnExecute') as $flush)
+ $this->loadFlushOnCache($cacheModel,$node,$flush);
+ }
+
+ /**
+ * Load the flush interval
+ * @param TSqlMapCacheModel cache model
+ * @param SimpleXmlElement cache node
+ */
+ protected function loadFlushInterval($cacheModel, $node)
+ {
+ $flushInterval = $node->xpath('flushInterval');
+ if($flushInterval === null || count($flushInterval) === 0) return;
+ $duration = 0;
+ foreach($flushInterval[0]->attributes() as $name=>$value)
+ {
+ switch(strToLower($name))
+ {
+ case 'seconds':
+ $duration += (integer)$value;
+ break;
+ case 'minutes':
+ $duration += 60 * (integer)$value;
+ break;
+ case 'hours':
+ $duration += 3600 * (integer)$value;
+ break;
+ case 'days':
+ $duration += 86400 * (integer)$value;
+ break;
+ case 'duration':
+ $duration = (integer)$value;
+ break 2; // switch, foreach
+ }
+ }
+ $cacheModel->setFlushInterval($duration);
+ }
+
+ /**
+ * Load the flush on cache properties.
+ * @param TSqlMapCacheModel cache model
+ * @param SimpleXmlElement parent node.
+ * @param SimpleXmlElement flush node.
+ */
+ protected function loadFlushOnCache($cacheModel,$parent,$node)
+ {
+ $id = $cacheModel->getID();
+ if(!isset($this->_FlushOnExecuteStatements[$id]))
+ $this->_FlushOnExecuteStatements[$id] = array();
+ foreach($node->attributes() as $name=>$value)
+ {
+ if(strtolower($name)==='statement')
+ $this->_FlushOnExecuteStatements[$id][] = (string)$value;
+ }
+ }
+
+ /**
+ * Attach CacheModel to statement and register trigger statements for cache models
+ */
+ protected function registerCacheTriggers()
+ {
+ foreach($this->_FlushOnExecuteStatements as $cacheID => $statementIDs)
+ {
+ $cacheModel = $this->_manager->getCacheModel($cacheID);
+ foreach($statementIDs as $statementID)
+ {
+ $statement = $this->_manager->getMappedStatement($statementID);
+ $cacheModel->registerTriggerStatement($statement);
+ }
+ }
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/DataMapper/TFastSqlMapApplicationCache.php b/lib/prado/framework/Data/SqlMap/DataMapper/TFastSqlMapApplicationCache.php
new file mode 100644
index 0000000..ec05978
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/DataMapper/TFastSqlMapApplicationCache.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * TFastSqlMapApplicationCache class file contains Fast SqlMap cache implementation.
+ *
+ * @author Berczi Gabor <gabor.berczi@devworx.hu>
+ * @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.SqlMap
+ */
+
+/**
+ * TFastSqlMapApplicationCache class file
+ *
+ * Fast SqlMap result cache class with minimal-concurrency get/set and atomic flush operations
+ *
+ * @author Berczi Gabor <gabor.berczi@devworx.hu>
+ * @package System.Data.SqlMap
+ * @since 3.2
+ */
+
+class TFastSqlMapApplicationCache implements ICache
+{
+ protected $_cacheModel=null;
+ protected $_cache=null;
+
+ public function __construct($cacheModel=null)
+ {
+ $this->_cacheModel = $cacheModel;
+ }
+
+ protected function getBaseKeyKeyName()
+ {
+ return 'SqlMapCacheBaseKey::'.$this->_cacheModel->getId();
+ }
+
+ protected function getBaseKey()
+ {
+ $cache = $this->getCache();
+ $keyname = $this->getBaseKeyKeyName();
+ $basekey = $cache->get($keyname);
+ if (!$basekey)
+ {
+ $basekey = DxUtil::generateRandomHash(8);
+ $cache->set($keyname,$basekey);
+ }
+ return $basekey;
+ }
+
+ protected function getCacheKey($key)
+ {
+ return $this->getBaseKey().'###'.$key;
+ }
+
+ public function delete($key)
+ {
+ $this->getCache()->delete($this->getCacheKey($key));
+ }
+
+ public function flush()
+ {
+ $this->getCache()->delete($this->getBaseKeyKeyName());
+ }
+
+ public function get($key)
+ {
+ $result = $this->getCache()->get($this->getCacheKey($key));
+ return $result === false ? null : $result;
+ }
+
+ public function set($key, $value,$expire=0,$dependency=null)
+ {
+ $this->getCache()->set($this->getCacheKey($key), $value, $expire,$dependency);
+ }
+
+ protected function getCache()
+ {
+ if (!$this->_cache)
+ $this->_cache = Prado::getApplication()->getCache();
+ return $this->_cache;
+ }
+
+ public function add($id,$value,$expire=0,$dependency=null)
+ {
+ throw new TSqlMapException('sqlmap_use_set_to_store_cache');
+ }
+}
diff --git a/lib/prado/framework/Data/SqlMap/DataMapper/TLazyLoadList.php b/lib/prado/framework/Data/SqlMap/DataMapper/TLazyLoadList.php
new file mode 100644
index 0000000..721c0fc
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/DataMapper/TLazyLoadList.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * TLazyLoadList, TObjectProxy 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.SqlMap
+ */
+
+/**
+ * TLazyLoadList executes mapped statements when the proxy collection is first accessed.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TLazyLoadList
+{
+ private $_param;
+ private $_target;
+ private $_propertyName='';
+ private $_statement='';
+ private $_loaded=false;
+ private $_innerList;
+ private $_connection;
+
+ /**
+ * Create a new proxy list that will execute the mapped statement when any
+ * of the list's method are accessed for the first time.
+ * @param TMappedStatement statement to be executed to load the data.
+ * @param mixed parameter value for the statement.
+ * @param object result object that contains the lazy collection.
+ * @param string property of the result object to set the loaded collection.
+ */
+ protected function __construct($mappedStatement, $param, $target, $propertyName)
+ {
+ $this->_param = $param;
+ $this->_target = $target;
+ $this->_statement = $mappedStatement;
+ $this->_connection=$mappedStatement->getManager()->getDbConnection();
+ $this->_propertyName = $propertyName;
+ }
+
+ /**
+ * Create a new instance of a lazy collection.
+ * @param TMappedStatement statement to be executed to load the data.
+ * @param mixed parameter value for the statement.
+ * @param object result object that contains the lazy collection.
+ * @param string property of the result object to set the loaded collection.
+ * @return TObjectProxy proxied collection object.
+ */
+ public static function newInstance($mappedStatement, $param, $target, $propertyName)
+ {
+ $handler = new self($mappedStatement, $param, $target, $propertyName);
+ $statement = $mappedStatement->getStatement();
+ $registry=$mappedStatement->getManager()->getTypeHandlers();
+ $list = $statement->createInstanceOfListClass($registry);
+ if(!is_object($list))
+ throw new TSqlMapExecutionException('sqlmap_invalid_lazyload_list',$statement->getID());
+ return new TObjectProxy($handler, $list);
+ }
+
+ /**
+ * Relay the method call to the underlying collection.
+ * @param string method name.
+ * @param array method parameters.
+ */
+ public function intercept($method, $arguments)
+ {
+ return call_user_func_array(array($this->_innerList, $method), $arguments);
+ }
+
+ /**
+ * Load the data by executing the mapped statement.
+ */
+ protected function fetchListData()
+ {
+ if($this->_loaded == false)
+ {
+ $this->_innerList = $this->_statement->executeQueryForList($this->_connection,$this->_param);
+ $this->_loaded = true;
+ //replace the target property with real list
+ TPropertyAccess::set($this->_target, $this->_propertyName, $this->_innerList);
+ }
+ }
+
+ /**
+ * Try to fetch the data when any of the proxy collection method is called.
+ * @param string method name.
+ * @return boolean true if the underlying collection has the corresponding method name.
+ */
+ public function hasMethod($method)
+ {
+ $this->fetchListData();
+ if(is_object($this->_innerList))
+ return in_array($method, get_class_methods($this->_innerList));
+ return false;
+ }
+}
+
+/**
+ * TObjectProxy sets up a simple object that intercepts method calls to a
+ * particular object and relays the call to handler object.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TObjectProxy
+{
+ private $_object;
+ private $_handler;
+
+ /**
+ * @param object handler to method calls.
+ * @param object the object to by proxied.
+ */
+ public function __construct($handler, $object)
+ {
+ $this->_handler = $handler;
+ $this->_object = $object;
+ }
+
+ /**
+ * Relay the method call to the handler object (if able to be handled), otherwise
+ * it calls the proxied object's method.
+ * @param string method name called
+ * @param array method arguments
+ * @return mixed method return value.
+ */
+ public function __call($method,$params)
+ {
+ if($this->_handler->hasMethod($method))
+ return $this->_handler->intercept($method, $params);
+ else
+ return call_user_func_array(array($this->_object, $method), $params);
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/DataMapper/TPropertyAccess.php b/lib/prado/framework/Data/SqlMap/DataMapper/TPropertyAccess.php
new file mode 100644
index 0000000..3aadc17
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/DataMapper/TPropertyAccess.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * TPropertyAccess 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.SqlMap
+ */
+
+/**
+ * TPropertyAccess class provides dot notation stype property access and setting.
+ *
+ * Access object's properties (and subproperties) using dot path notation.
+ * The following are equivalent.
+ * <code>
+ * echo $obj->property1;
+ * echo $obj->getProperty1();
+ * echo $obj['property1']; //$obj may be an array or object
+ * echo TPropertyAccess($obj, 'property1');
+ * </code>
+ *
+ * Setting a property value.
+ * <code>
+ * $obj1->propert1 = 'hello';
+ * $obj->setProperty('hello');
+ * $obj['property1'] = 'hello'; //$obj may be an array or object
+ * TPropertyAccess($obj, 'property1', 'hello');
+ * </code>
+ *
+ * Subproperties are supported using the dot notation. E.g.
+ * <code>
+ * echo $obj->property1->property2->property3
+ * echo TPropertyAccess::get($obj, 'property1.property2.property3');
+ * </code>
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TPropertyAccess
+{
+ /**
+ * Gets the property value.
+ * @param mixed object or path.
+ * @param string property path.
+ * @return mixed property value.
+ * @throws TInvalidDataValueException if property path is invalid.
+ */
+ public static function get($object,$path)
+ {
+ if(!is_array($object) && !is_object($object))
+ return $object;
+ $properties = explode('.', $path);
+ foreach($properties as $prop)
+ {
+ if(is_array($object) || $object instanceof ArrayAccess)
+ {
+ if(array_key_exists($prop, $object))
+ $object = $object[$prop];
+ else
+ throw new TInvalidPropertyException('sqlmap_invalid_property',$path);
+ }
+ else if(is_object($object))
+ {
+ $getter = 'get'.$prop;
+ if(method_exists($object, $getter) && is_callable(array($object, $getter)))
+ $object = $object->{$getter}();
+ else if(in_array($prop, array_keys(get_object_vars($object))))
+ $object = $object->{$prop};
+ elseif(method_exists($object, '__get') && is_callable(array($object, '__get')))
+ $object = $object->{$prop};
+ else
+ throw new TInvalidPropertyException('sqlmap_invalid_property',$path);
+ }
+ else
+ throw new TInvalidPropertyException('sqlmap_invalid_property',$path);
+ }
+ return $object;
+ }
+
+ /**
+ * @param mixed object or array
+ * @param string property path.
+ * @return boolean true if property path is valid
+ */
+ public static function has($object, $path)
+ {
+ if(!is_array($object) && !is_object($object))
+ return false;
+ $properties = explode('.', $path);
+ foreach($properties as $prop)
+ {
+ if(is_array($object) || $object instanceof ArrayAccess)
+ {
+ if(array_key_exists($prop, $object))
+ $object = $object[$prop];
+ else
+ return false;
+ }
+ else if(is_object($object))
+ {
+ $getter = 'get'.$prop;
+ if(method_exists($object, $getter) && is_callable(array($object, $getter)))
+ $object = $object->{$getter}();
+ else if(in_array($prop, array_keys(get_object_vars($object))))
+ $object = $object->{$prop};
+ elseif(method_exists($object, '__get') && is_callable(array($object, '__get')))
+ $object = $object->{$prop};
+ else
+ return false;
+ }
+ else
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Sets the property value.
+ * @param mixed object or array
+ * @param string property path.
+ * @param mixed new property value.
+ * @throws TInvalidDataValueException if property path is invalid.
+ */
+ public static function set(&$originalObject, $path, $value)
+ {
+ $properties = explode('.', $path);
+ $prop = array_pop($properties);
+ if(count($properties) > 0)
+ $object = self::get($originalObject, implode('.',$properties));
+ else
+ $object = &$originalObject;
+
+ if(is_array($object) || $object instanceof ArrayAccess)
+ {
+ $object[$prop] = $value;
+ }
+ else if(is_object($object))
+ {
+ $setter = 'set'.$prop;
+ if (method_exists($object, $setter) && is_callable(array($object, $setter)))
+ $object->{$setter}($value);
+ else
+ $object->{$prop} = $value;
+ }
+ else
+ throw new TInvalidPropertyException('sqlmap_invalid_property_type',$path);
+ }
+
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapCache.php b/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapCache.php
new file mode 100644
index 0000000..2df52eb
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapCache.php
@@ -0,0 +1,290 @@
+<?php
+/**
+ * TSqlMapCache class file contains FIFO, LRU, and GLOBAL cache implementations.
+ *
+ * @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.SqlMap
+ */
+
+/**
+ * Allow different implementation of caching strategy. See <tt>TSqlMapFifoCache</tt>
+ * for a first-in-first-out implementation. See <tt>TSqlMapLruCache</tt> for
+ * a least-recently-used cache implementation.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+abstract class TSqlMapCache implements ICache
+{
+ protected $_keyList;
+ protected $_cache;
+ protected $_cacheSize = 100;
+ protected $_cacheModel = null;
+
+ /**
+ * Create a new cache with limited cache size.
+ * @param TSqlMapCacheModel $cacheModel.
+ */
+ public function __construct($cacheModel=null)
+ {
+ $this->_cache = new TMap;
+ $this->_keyList = new TList;
+ $this->_cacheModel=$cacheModel;
+ }
+
+ /**
+ * Maximum number of items to cache. Default size is 100.
+ * @param int cache size.
+ */
+ public function setCacheSize($value)
+ {
+ $this->_cacheSize=TPropertyValue::ensureInteger($value,100);
+ }
+
+ /**
+ * @return int cache size.
+ */
+ public function getCacheSize()
+ {
+ return $this->_cacheSize;
+ }
+
+ /**
+ * @return object the object removed if exists, null otherwise.
+ */
+ public function delete($key)
+ {
+ $object = $this->get($key);
+ $this->_cache->remove($key);
+ $this->_keyList->remove($key);
+ return $object;
+ }
+
+ /**
+ * Clears the cache.
+ */
+ public function flush()
+ {
+ $this->_keyList->clear();
+ $this->_cache->clear();
+ }
+
+ /**
+ * @throws TSqlMapException not implemented.
+ */
+ public function add($id,$value,$expire=0,$dependency=null)
+ {
+ throw new TSqlMapException('sqlmap_use_set_to_store_cache');
+ }
+}
+
+/**
+ * First-in-First-out cache implementation, removes
+ * object that was first added when the cache is full.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapFifoCache extends TSqlMapCache
+{
+ /**
+ * @return mixed Gets a cached object with the specified key.
+ */
+ public function get($key)
+ {
+ return $this->_cache->itemAt($key);
+ }
+
+ /**
+ * Stores a value identified by a key into cache.
+ * The expire and dependency parameters are ignored.
+ * @param string cache key
+ * @param mixed value to cache.
+ */
+ public function set($key, $value,$expire=0,$dependency=null)
+ {
+ $this->_cache->add($key, $value);
+ $this->_keyList->add($key);
+ if($this->_keyList->getCount() > $this->_cacheSize)
+ {
+ $oldestKey = $this->_keyList->removeAt(0);
+ $this->_cache->remove($oldestKey);
+ }
+ }
+}
+
+/**
+ * Least recently used cache implementation, removes
+ * object that was accessed last when the cache is full.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapLruCache extends TSqlMapCache
+{
+ /**
+ * @return mixed Gets a cached object with the specified key.
+ */
+ public function get($key)
+ {
+ if($this->_keyList->contains($key))
+ {
+ $this->_keyList->remove($key);
+ $this->_keyList->add($key);
+ return $this->_cache->itemAt($key);
+ }
+ }
+
+ /**
+ * Stores a value identified by a key into cache.
+ * The expire and dependency parameters are ignored.
+ * @param string the key identifying the value to be cached
+ * @param mixed the value to be cached
+ */
+ public function set($key, $value,$expire=0,$dependency=null)
+ {
+ $this->_cache->add($key, $value);
+ $this->_keyList->add($key);
+ if($this->_keyList->getCount() > $this->_cacheSize)
+ {
+ $oldestKey = $this->_keyList->removeAt(0);
+ $this->_cache->remove($oldestKey);
+ }
+ }
+}
+
+/**
+ * TSqlMapApplicationCache uses the default Prado application cache for
+ * caching SqlMap results.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapApplicationCache implements ICache
+{
+ protected $_cacheModel=null;
+
+ /**
+ * Create a new cache with limited cache size.
+ * @param TSqlMapCacheModel $cacheModel.
+ */
+ public function __construct($cacheModel=null)
+ {
+ $this->_cacheModel=$cacheModel;
+ }
+
+ /**
+ *
+ * @return string a KeyListID for the cache model.
+ */
+ protected function getKeyListId()
+ {
+ $id='keyList';
+ if ($this->_cacheModel instanceof TSqlMapCacheModel)
+ $id.='_'.$this->_cacheModel->getId();
+ return $id;
+ }
+ /**
+ * Retreive keylist from cache or create it if it doesn't exists
+ * @return TList
+ */
+ protected function getKeyList()
+ {
+ if (($keyList=$this->getCache()->get($this->getKeyListId()))===false)
+ {
+ $keyList=new TList();
+ $this->getCache()->set($this->getKeyListId(), $keyList);
+ }
+ return $keyList;
+ }
+
+ protected function setKeyList($keyList)
+ {
+ $this->getCache()->set($this->getKeyListId(), $keyList);
+ }
+
+ /**
+ * @param string item to be deleted.
+ */
+ public function delete($key)
+ {
+ $keyList=$this->getKeyList();
+ $keyList->remove($key);
+ $this->getCache()->delete($key);
+ $this->setKeyList($keyList);
+ }
+
+ /**
+ * Deletes all items in the cache, only for data cached by sqlmap cachemodel
+ */
+ public function flush()
+ {
+ $keyList=$this->getKeyList();
+ $cache=$this->getCache();
+ foreach ($keyList as $key)
+ {
+ $cache->delete($key);
+ }
+ // Remove the old keylist
+ $cache->delete($this->getKeyListId());
+ }
+
+ /**
+ * @return mixed Gets a cached object with the specified key.
+ */
+ public function get($key)
+ {
+ $result = $this->getCache()->get($key);
+ if ($result === false)
+ {
+ // if the key has not been found in cache (e.g expired), remove from keylist
+ $keyList=$this->getKeyList();
+ if ($keyList->contains($key))
+ {
+ $keyList->remove($key);
+ $this->setKeyList($keyList);
+ }
+ }
+ return $result === false ? null : $result;
+ }
+
+ /**
+ * Stores a value identified by a key into cache.
+ * @param string the key identifying the value to be cached
+ * @param mixed the value to be cached
+ */
+ public function set($key, $value,$expire=0,$dependency=null)
+ {
+ $this->getCache()->set($key, $value, $expire,$dependency);
+ $keyList=$this->getKeyList();
+ if (!$keyList->contains($key))
+ {
+ $keyList->add($key);
+ $this->setKeyList($keyList);
+ }
+ }
+
+ /**
+ * @return ICache Application cache instance.
+ */
+ protected function getCache()
+ {
+ return Prado::getApplication()->getCache();
+ }
+
+ /**
+ * @throws TSqlMapException not implemented.
+ */
+ public function add($id,$value,$expire=0,$dependency=null)
+ {
+ throw new TSqlMapException('sqlmap_use_set_to_store_cache');
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapException.php b/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapException.php
new file mode 100644
index 0000000..bce03a5
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapException.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * TSqlMapException is the base exception class for all SqlMap exceptions.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapException extends TException
+{
+ /**
+ * Constructor, similar to the parent constructor. For parameters that
+ * are of SimpleXmlElement, the tag name and its attribute names and values
+ * are expanded into a string.
+ */
+ public function __construct($errorMessage)
+ {
+ $this->setErrorCode($errorMessage);
+ $errorMessage=$this->translateErrorMessage($errorMessage);
+ $args=func_get_args();
+ array_shift($args);
+ $n=count($args);
+ $tokens=array();
+ for($i=0;$i<$n;++$i)
+ {
+ if($args[$i] instanceof SimpleXmlElement)
+ $tokens['{'.$i.'}']=$this->implodeNode($args[$i]);
+ else
+ $tokens['{'.$i.'}']=TPropertyValue::ensureString($args[$i]);
+ }
+ parent::__construct(strtr($errorMessage,$tokens));
+ }
+
+ /**
+ * @param SimpleXmlElement node
+ * @return string tag name and attribute names and values.
+ */
+ protected function implodeNode($node)
+ {
+ $attributes=array();
+ foreach($node->attributes() as $k=>$v)
+ $attributes[]=$k.'="'.(string)$v.'"';
+ return '<'.$node->getName().' '.implode(' ',$attributes).'>';
+ }
+
+ /**
+ * @return string path to the error message file
+ */
+ protected function getErrorMessageFile()
+ {
+ $lang=Prado::getPreferredLanguage();
+ $dir=dirname(__FILE__);
+ $msgFile=$dir.'/messages-'.$lang.'.txt';
+ if(!is_file($msgFile))
+ $msgFile=$dir.'/messages.txt';
+ return $msgFile;
+ }
+}
+
+/**
+ * TSqlMapConfigurationException, raised during configuration file parsing.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapConfigurationException extends TSqlMapException
+{
+
+}
+
+/**
+ * TSqlMapUndefinedException, raised when mapped statemented are undefined.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapUndefinedException extends TSqlMapException
+{
+
+}
+
+/**
+ * TSqlMapDuplicateException, raised when a duplicate mapped statement is found.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapDuplicateException extends TSqlMapException
+{
+}
+
+/**
+ * TInvalidPropertyException, raised when setting or getting an invalid property.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TInvalidPropertyException extends TSqlMapException
+{
+}
+
+class TSqlMapExecutionException extends TSqlMapException
+{
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapPagedList.php b/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapPagedList.php
new file mode 100644
index 0000000..0bf9fdc
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapPagedList.php
@@ -0,0 +1,206 @@
+<?php
+/**
+ * TSqlMapPagedList 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.SqlMap
+ */
+
+Prado::using('System.Collections.TPagedList');
+
+/**
+ * TSqlMapPagedList implements a list with paging functionality that retrieves
+ * data from a SqlMap statement.
+ *
+ * The maximum number of records fetched is 3 times the page size. It fetches
+ * the current, the previous and the next page at a time. This allows the paged
+ * list to determine if the page is a the begin, the middle or the end of the list.
+ *
+ * The paged list does not need to know about the total number of records.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapPagedList extends TPagedList
+{
+ private $_statement;
+ private $_parameter;
+ private $_prevPageList;
+ private $_nextPageList;
+ private $_delegate=null;
+
+ /**
+ * Create a new SqlMap paged list.
+ * @param IMappedStatement SqlMap statement.
+ * @param mixed query parameters
+ * @param int page size
+ * @param mixed delegate for each data row retrieved.
+ * @param int number of page to fetch on initialization
+ */
+ public function __construct(IMappedStatement $statement,$parameter, $pageSize, $delegate=null, $page=0)
+ {
+ parent::__construct();
+ parent::setCustomPaging(true);
+ $this->initialize($statement,$parameter, $pageSize, $page);
+ $this->_delegate=$delegate;
+ }
+
+ /**
+ * Initialize the paged list.
+ * @param IMappedStatement SqlMap statement.
+ * @param mixed query parameters
+ * @param int page size.
+ * @param int number of page.
+ */
+ protected function initialize($statement, $parameter, $pageSize, $page)
+ {
+ $this->_statement = $statement;
+ $this->_parameter = $parameter;
+ $this->setPageSize($pageSize);
+ $this->attachEventHandler('OnFetchData', array($this, 'fetchDataFromStatement'));
+ $this->gotoPage($page);
+ }
+
+ /**
+ * @throws TSqlMapException custom paging must be enabled.
+ */
+ public function setCustomPaging($value)
+ {
+ throw new TSqlMapException('sqlmap_must_enable_custom_paging');
+ }
+
+ /**
+ * Fetch data by executing the SqlMap statement.
+ * @param TPageList current object.
+ * @param TPagedListFetchDataEventParameter fetch parameters
+ */
+ protected function fetchDataFromStatement($sender, $param)
+ {
+ $limit = $this->getOffsetAndLimit($param);
+ $connection = $this->_statement->getManager()->getDbConnection();
+ $data = $this->_statement->executeQueryForList($connection,
+ $this->_parameter, null, $limit[0], $limit[1], $this->_delegate);
+ $this->populateData($param, $data);
+ }
+
+ /**
+ * Switches to the next page.
+ * @return integer|boolean the new page index, false if next page is not availabe.
+ */
+ public function nextPage()
+ {
+ return $this->getIsNextPageAvailable() ? parent::nextPage() : false;
+ }
+
+ /**
+ * Switches to the previous page.
+ * @return integer|boolean the new page index, false if previous page is not availabe.
+ */
+ public function previousPage()
+ {
+ return $this->getIsPreviousPageAvailable() ? parent::previousPage() : false;
+ }
+
+ /**
+ * Populate the list with the fetched data.
+ * @param TPagedListFetchDataEventParameter fetch parameters
+ * @param array fetched data.
+ */
+ protected function populateData($param, $data)
+ {
+ $total = $data instanceof TList ? $data->getCount() : count($data);
+ $pageSize = $this->getPageSize();
+ if($total < 1)
+ {
+ $param->setData($data);
+ $this->_prevPageList = null;
+ $this->_nextPageList = null;
+ return;
+ }
+
+ if($param->getNewPageIndex() < 1)
+ {
+ $this->_prevPageList = null;
+ if($total <= $pageSize)
+ {
+ $param->setData($data);
+ $this->_nextPageList = null;
+ }
+ else
+ {
+ $param->setData(array_slice($data, 0, $pageSize));
+ $this->_nextPageList = array_slice($data, $pageSize-1,$total);
+ }
+ }
+ else
+ {
+ if($total <= $pageSize)
+ {
+ $this->_prevPageList = array_slice($data, 0, $total);
+ $param->setData(array());
+ $this->_nextPageList = null;
+ }
+ else if($total <= $pageSize*2)
+ {
+ $this->_prevPageList = array_slice($data, 0, $pageSize);
+ $param->setData(array_slice($data, $pageSize, $total));
+ $this->_nextPageList = null;
+ }
+ else
+ {
+ $this->_prevPageList = array_slice($data, 0, $pageSize);
+ $param->setData(array_slice($data, $pageSize, $pageSize));
+ $this->_nextPageList = array_slice($data, $pageSize*2, $total-$pageSize*2);
+ }
+ }
+ }
+
+ /**
+ * Calculate the data fetch offsets and limits.
+ * @param TPagedListFetchDataEventParameter fetch parameters
+ * @return array 1st element is the offset, 2nd element is the limit.
+ */
+ protected function getOffsetAndLimit($param)
+ {
+ $index = $param->getNewPageIndex();
+ $pageSize = $this->getPageSize();
+ return $index < 1 ? array($index, $pageSize*2) : array(($index-1)*$pageSize, $pageSize*3);
+ }
+
+ /**
+ * @return boolean true if the next page is available, false otherwise.
+ */
+ public function getIsNextPageAvailable()
+ {
+ return $this->_nextPageList!==null;
+ }
+
+ /**
+ * @return boolean true if the previous page is available, false otherwise.
+ */
+ public function getIsPreviousPageAvailable()
+ {
+ return $this->_prevPageList!==null;
+ }
+
+ /**
+ * @return boolean true if is the very last page, false otherwise.
+ */
+ public function getIsLastPage()
+ {
+ return ($this->_nextPageList===null) || $this->_nextPageList->getCount() < 1;
+ }
+
+ /**
+ * @return boolean true if is not first nor last page, false otherwise.
+ */
+ public function getIsMiddlePage()
+ {
+ return !($this->getIsFirstPage() || $this->getIsLastPage());
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapTypeHandlerRegistry.php b/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapTypeHandlerRegistry.php
new file mode 100644
index 0000000..7941426
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/DataMapper/TSqlMapTypeHandlerRegistry.php
@@ -0,0 +1,189 @@
+<?php
+/**
+ * TSqlMapTypeHandlerRegistry, and abstract TSqlMapTypeHandler 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.SqlMap
+ */
+
+/**
+ * TTypeHandlerFactory provides type handler classes to convert database field type
+ * to PHP types and vice versa.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapTypeHandlerRegistry
+{
+ private $_typeHandlers=array();
+
+ /**
+ * @param string database field type
+ * @return TSqlMapTypeHandler type handler for give database field type.
+ */
+ public function getDbTypeHandler($dbType='NULL')
+ {
+ foreach($this->_typeHandlers as $handler)
+ if($handler->getDbType()===$dbType)
+ return $handler;
+ }
+
+ /**
+ * @param string type handler class name
+ * @return TSqlMapTypeHandler type handler
+ */
+ public function getTypeHandler($class)
+ {
+ if(isset($this->_typeHandlers[$class]))
+ return $this->_typeHandlers[$class];
+ }
+
+ /**
+ * @param TSqlMapTypeHandler registers a new type handler
+ */
+ public function registerTypeHandler(TSqlMapTypeHandler $handler)
+ {
+ $this->_typeHandlers[$handler->getType()] = $handler;
+ }
+
+ /**
+ * Creates a new instance of a particular class (for PHP primative types,
+ * their corresponding default value for given type is used).
+ * @param string PHP type name
+ * @return mixed default type value, if no type is specified null is returned.
+ * @throws TSqlMapException if class name is not found.
+ */
+ public function createInstanceOf($type='')
+ {
+ if(strlen($type) > 0)
+ {
+ switch(strtolower($type))
+ {
+ case 'string': return '';
+ case 'array': return array();
+ case 'float': case 'double': case 'decimal': return 0.0;
+ case 'integer': case 'int': return 0;
+ case 'bool': case 'boolean': return false;
+ }
+
+ if(class_exists('Prado', false))
+ return Prado::createComponent($type);
+ else if(class_exists($type, false)) //NO auto loading
+ return new $type;
+ else
+ throw new TSqlMapException('sqlmap_unable_to_find_class', $type);
+ }
+ }
+
+ /**
+ * Converts the value to given type using PHP's settype() function.
+ * @param string PHP primative type.
+ * @param mixed value to be casted
+ * @return mixed type casted value.
+ */
+ public function convertToType($type, $value)
+ {
+ switch(strtolower($type))
+ {
+ case 'integer': case 'int':
+ $type = 'integer'; break;
+ case 'float': case 'double': case 'decimal':
+ $type = 'float'; break;
+ case 'boolean': case 'bool':
+ $type = 'boolean'; break;
+ case 'string' :
+ $type = 'string'; break;
+ default:
+ return $value;
+ }
+ settype($value, $type);
+ return $value;
+ }
+}
+
+/**
+ * A simple interface for implementing custom type handlers.
+ *
+ * Using this interface, you can implement a type handler that
+ * will perform customized processing before parameters are set
+ * on and after values are retrieved from the database.
+ * Using a custom type handler you can extend
+ * the framework to handle types that are not supported, or
+ * handle supported types in a different way. For example,
+ * you might use a custom type handler to implement proprietary
+ * BLOB support (e.g. Oracle), or you might use it to handle
+ * booleans using "Y" and "N" instead of the more typical 0/1.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+abstract class TSqlMapTypeHandler extends TComponent
+{
+ private $_dbType='NULL';
+ private $_type;
+ /**
+ * @param string database field type.
+ */
+ public function setDbType($value)
+ {
+ $this->_dbType=$value;
+ }
+
+ /**
+ * @return string database field type.
+ */
+ public function getDbType()
+ {
+ return $this->_dbType;
+ }
+
+ public function getType()
+ {
+ if($this->_type===null)
+ return get_class($this);
+ else
+ return $this->_type;
+ }
+
+ public function setType($value)
+ {
+ $this->_type=$value;
+ }
+
+ /**
+ * Performs processing on a value before it is used to set
+ * the parameter of a IDbCommand.
+ * @param object The interface for setting the value.
+ * @param object The value to be set.
+ */
+ public abstract function getParameter($object);
+
+
+ /**
+ * Performs processing on a value before after it has been retrieved
+ * from a database
+ * @param object The interface for getting the value.
+ * @return mixed The processed value.
+ */
+ public abstract function getResult($string);
+
+
+ /**
+ * Casts the string representation of a value into a type recognized by
+ * this type handler. This method is used to translate nullValue values
+ * into types that can be appropriately compared. If your custom type handler
+ * cannot support nullValues, or if there is no reasonable string representation
+ * for this type (e.g. File type), you can simply return the String representation
+ * as it was passed in. It is not recommended to return null, unless null was passed
+ * in.
+ * @param array result row.
+ * @return mixed
+ */
+ public abstract function createNewInstance($row=null);
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/DataMapper/messages.txt b/lib/prado/framework/Data/SqlMap/DataMapper/messages.txt
new file mode 100644
index 0000000..0923d60
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/DataMapper/messages.txt
@@ -0,0 +1,66 @@
+
+# TSqlMapManager.php
+sqlmap_contains_no_statement = Unable to find SQLMap statement '{0}'.
+sqlmap_already_contains_statement = Duplicate SQLMap statement found, '{0}' already exists.
+sqlmap_contains_no_result_map = Unable to find SQLMap result map '{0}'.
+sqlmap_already_contains_result_map = Duplicate SQLMap result map found, '{0}' already exists.
+sqlmap_contains_no_parameter_map = Unable to find SQLMap parameter map '{0}'.
+sqlmap_already_contains_parameter_map = Duplicate SQLMap parameter map found, '{0}' already exists.
+sqlmap_cache_model_already_exists = This SQLMap already contains cache model '{0}'.
+sqlmap_unable_to_find_cache_model = Unable to find cache model '{0}' in this SQLMap.
+
+# TTypeHandlerFactory.php
+sqlmap_dbtype_handler_not_found = Type handler for dbType='{0}' not found.
+sqlmap_type_handler_class_not_found = Type handler class '{0}' not found.
+sqlmap_unable_to_find_class = Unable to find class '{0}'.
+
+# TSqlMapXmlConfig.php
+sqlmap_node_class_undef = Missing attribute 'class' in tag '{0}' in configuration file '{1}'.
+sqlmap_unable_to_find_parent_result_map = Unable to find parent SQLMap result map named '{2}' in file {1} near '{0}'.
+sqlmap_undefined_discriminator = The <discriminator> tag not found in ResultMap '{0}' for sub-map '{2}' in file '{1}'.
+sqlmap_unable_to_find_parent_sql = Unable to find parent sql statement extension '{0}' near '{2}' in file {1}.
+sqlmap_invalid_property = Invalid property '{0}' for class '{1}' for tag '{2}' in configuration file '{3}'.
+
+
+# TInlineParameterMapParser.php
+sqlmap_undefined_property_inline_map = Invalid attribute '{0}' in '{3}' for inline parameter in statement '{2}' in file {1}.
+
+# TSqlMapCacheModel.php
+sqlmap_unable_to_find_implemenation = Unable to find cache implementation class '{0}'.
+
+# TResultProperty.php
+sqlmap_error_in_result_property_from_handler = For result map '{0}', error in getting result from type handler '{2}', with value '{1}'.
+
+# TParameterMap.php
+sqlmap_index_must_be_string_or_int = Invalid index '{0}', must be an integes or string to get a SqlMap parameter map property.
+sqlmap_unable_to_get_property_for_parameter = Unable to find property '{1}' in object '{2}' for parameter map '{0}'.
+sqlmap_error_in_parameter_from_handler = For parameter map '{0}', error in getting parameter from type handler '{2}' with value '{1}': '{3}'.
+
+# MISC
+sqlmap_type_handler_class_undef = Unable to find type handler class named '{1}' in sqlmap configuration file '{0}'.
+sqlmap_type_handler_callback_undef = Attributes 'type' and 'callback' must be defined in typeHandler tag in configuration file '{0}'.
+
+sqlmap_undefined_attribute = {0} attribute '{1}' is not defined for {2} in file {3}.
+sqlmap_unable_to_find_parent_parameter_map = Unable to find parent parameter map extension '{0}' in file {1}.
+sqlmap_unable_to_find_result_mapping = Unable to resolve SQLMap result mapping '{0}' in Result Map '{2}' using configuration file {1}.
+
+sqlmap_undefined_input_property = Undefined array index '{0}' in retrieving property in SQLMap parameter map '{1}'.
+sqlmap_can_not_instantiate = Type handler '{0}' can not create new objects.
+sqlmap_cannot_execute_query_for_map = SQLMap statement class {0} can not query for map in statement '{1}'.
+sqlmap_cannot_execute_update = SQLMap statement class {0} can not execute update query in statement '{1}'.
+sqlmap_cannot_execute_insert = SQLMap statement class {0} can not execute insert in statement '{1}'.
+sqlmap_cannot_execute_query_for_list = SQLMap statement class {0} can not query for list in statement '{1}'.
+sqlmap_cannot_execute_query_for_object = SQLMap statement class {0} can not query for object in statement '{1}'.
+sqlmap_execution_error_no_record = No record set found in executing statement '{0}': '{1}'.
+sqlmap_unable_to_create_new_instance = Unable to create a new instance of '{0}' using type hander '{1}' for SQLMap statement with ID '{2}'.
+sqlmap_invalid_property_type = Invalid object type, must be 'Object', unable to set property in path '{0}'.
+
+sqlmap_unable_to_find_config = Unable to find SQLMap configuration file '{0}'.
+sqlmap_unable_to_find_groupby = Unable to find data in result set with column '{0}' in result map with ID '{1}'.
+sqlmap_invalid_lazyload_list = Invalid type to lazy load, must specify a valid ListClass in statement '{0}'.
+sqlmap_unable_to_find_resource = 'Unable to find SQLMap configuration file '{0}'.
+sqlmap_query_execution_error = Error in executing SQLMap statement '{0}' : '{1}'.
+sqlmap_invalid_delegate = Invalid callback row delegate '{1}' in mapped statement '{0}'.
+sqlmap_invalid_prado_cache = Unable to find Prado cache module for SQLMap cache '{0}'.
+
+sqlmap_non_groupby_array_list_type = Expecting GroupBy property in result map '{0}' since {1}::{2} is an array or TList type. \ No newline at end of file
diff --git a/lib/prado/framework/Data/SqlMap/Statements/IMappedStatement.php b/lib/prado/framework/Data/SqlMap/Statements/IMappedStatement.php
new file mode 100644
index 0000000..859d2ee
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/IMappedStatement.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * IMappedStatement interface file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ */
+
+/**
+ * Interface for all mapping statements.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+interface IMappedStatement
+{
+ /**
+ * @return string Name used to identify the MappedStatement amongst the others.
+ */
+ public function getID();
+
+ /**
+ * @return TSqlMapStatement The SQL statment used by this TMappedStatement.
+ */
+ public function getStatement();
+
+ /**
+ * @return TSqlMap The TSqlMap used by this TMappedStatement
+ */
+ public function getManager();
+
+ /**
+ * Executes the SQL and retuns all rows selected in a map that is keyed on
+ * the property named in the <tt>$keyProperty</tt> parameter. The value at
+ * each key will be the value of the property specified in the
+ * <tt>$valueProperty</tt> parameter. If <tt>$valueProperty</tt> is
+ * <tt>null</tt>, the entire result object will be entered.
+ * @param IDbConnection database connection to execute the query
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param string The property of the result object to be used as the key.
+ * @param string The property of the result object to be used as the value (or null)
+ * @return TMap A map of object containing the rows keyed by <tt>$keyProperty</tt>.
+ */
+ public function executeQueryForMap($connection, $parameter, $keyProperty, $valueProperty=null);
+
+
+ /**
+ * Execute an update statement. Also used for delete statement. Return the
+ * number of row effected.
+ * @param IDbConnection database connection to execute the query
+ * @param mixed The object used to set the parameters in the SQL.
+ * @return integer The number of row effected.
+ */
+ public function executeUpdate($connection, $parameter);
+
+
+ /**
+ * Executes the SQL and retuns a subset of the rows selected.
+ * @param IDbConnection database connection to execute the query
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param TList A list to populate the result with.
+ * @param integer The number of rows to skip over.
+ * @param integer The maximum number of rows to return.
+ * @return TList A TList of result objects.
+ */
+ public function executeQueryForList($connection, $parameter, $result=null, $skip=-1, $max=-1);
+
+
+ /**
+ * Executes an SQL statement that returns a single row as an object
+ * of the type of the <tt>$result</tt> passed in as a parameter.
+ * @param IDbConnection database connection to execute the query
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param object The result object.
+ * @return object result.
+ */
+ public function executeQueryForObject($connection,$parameter, $result=null);
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TCachingStatement.php b/lib/prado/framework/Data/SqlMap/Statements/TCachingStatement.php
new file mode 100644
index 0000000..067b7b9
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TCachingStatement.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * TCachingStatement 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.SqlMap.Statements
+ */
+
+/**
+ * TCacheingStatement class.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TCachingStatement extends TComponent implements IMappedStatement
+{
+ private $_mappedStatement;
+
+ public function __construct(TMappedStatement $statement)
+ {
+ $this->_mappedStatement = $statement;
+ }
+
+ public function getID()
+ {
+ return $this->_mappedStatement->getID();
+ }
+
+ public function getStatement()
+ {
+ return $this->_mappedStatement->getStatement();
+ }
+
+ public function getManager()
+ {
+ return $this->_mappedStatement->getManager();
+ }
+
+ public function executeQueryForMap($connection, $parameter,$keyProperty, $valueProperty=null, $skip=-1, $max=-1,$delegate=null)
+ {
+ $sql = $this->createCommand($connection, $parameter, $skip, $max);
+ $key = $this->getCacheKey(array(clone($sql), $keyProperty, $valueProperty,$skip, $max));
+ $map = $this->getStatement()->getCache()->get($key);
+ if($map===null)
+ {
+ $map = $this->_mappedStatement->runQueryForMap(
+ $connection, $parameter, $sql, $keyProperty, $valueProperty, $delegate);
+ $this->getStatement()->getCache()->set($key, $map);
+ }
+ return $map;
+ }
+
+ public function executeUpdate($connection, $parameter)
+ {
+ return $this->_mappedStatement->executeUpdate($connection, $parameter);
+ }
+
+ public function executeInsert($connection, $parameter)
+ {
+ return $this->executeInsert($connection, $parameter);
+ }
+
+ public function executeQueryForList($connection, $parameter, $result=null, $skip=-1, $max=-1, $delegate=null)
+ {
+ $sql = $this->createCommand($connection, $parameter, $skip, $max);
+ $key = $this->getCacheKey(array(clone($sql), $parameter, $skip, $max));
+ $list = $this->getStatement()->getCache()->get($key);
+ if($list===null)
+ {
+ $list = $this->_mappedStatement->runQueryForList(
+ $connection, $parameter, $sql, $result, $delegate);
+ $this->getStatement()->getCache()->set($key, $list);
+ }
+ return $list;
+ }
+
+ public function executeQueryForObject($connection, $parameter, $result=null)
+ {
+ $sql = $this->createCommand($connection, $parameter);
+ $key = $this->getCacheKey(array(clone($sql), $parameter));
+ $object = $this->getStatement()->getCache()->get($key);
+ if($object===null)
+ {
+ $object = $this->_mappedStatement->runQueryForObject($connection, $sql, $result);
+ $this->getStatement()->getCache()->set($key, $object);
+ }
+ return $object;
+ }
+
+ protected function getCacheKey($object)
+ {
+ $cacheKey = new TSqlMapCacheKey($object);
+ return $cacheKey->getHash();
+ }
+
+ protected function createCommand($connection, $parameter, $skip=null, $max=null)
+ {
+ return $this->_mappedStatement->getCommand()->create($this->getManager(),
+ $connection, $this->getStatement(), $parameter, $skip, $max);
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TDeleteMappedStatement.php b/lib/prado/framework/Data/SqlMap/Statements/TDeleteMappedStatement.php
new file mode 100644
index 0000000..fac34fa
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TDeleteMappedStatement.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * TDeleteMappedStatement 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.SqlMap.Statements
+ */
+
+/**
+ * TDeleteMappedStatement class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TDeleteMappedStatement extends TUpdateMappedStatement
+{
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TInsertMappedStatement.php b/lib/prado/framework/Data/SqlMap/Statements/TInsertMappedStatement.php
new file mode 100644
index 0000000..0673bcc
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TInsertMappedStatement.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * TInsertMappedStatement 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.SqlMap.Statements
+ */
+
+/**
+ * TInsertMappedStatement class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TInsertMappedStatement extends TMappedStatement
+{
+ public function executeQueryForMap($connection, $parameter,
+ $keyProperty, $valueProperty=null)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_query_for_map', get_class($this), $this->getID());
+ }
+
+ public function executeUpdate($connection, $parameter)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_update', get_class($this), $this->getID());
+ }
+
+ public function executeQueryForList($connection, $parameter, $result=null,
+ $skip=-1, $max=-1)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_query_for_list', get_class($this), $this->getID());
+ }
+
+ public function executeQueryForObject($connection, $parameter, $result=null)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_query_for_object', get_class($this), $this->getID());
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TMappedStatement.php b/lib/prado/framework/Data/SqlMap/Statements/TMappedStatement.php
new file mode 100644
index 0000000..2887114
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TMappedStatement.php
@@ -0,0 +1,1236 @@
+<?php
+/**
+ * TMappedStatement and related classes.
+ *
+ * @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.SqlMap.Statements
+ */
+
+/**
+ * TMappedStatement class executes SQL mapped statements. Mapped Statements can
+ * hold any SQL statement and use Parameter Maps and Result Maps for input and output.
+ *
+ * This class is usualy instantiated during SQLMap configuration by TSqlDomBuilder.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.0
+ */
+class TMappedStatement extends TComponent implements IMappedStatement
+{
+ /**
+ * @var TSqlMapStatement current SQL statement.
+ */
+ private $_statement;
+
+ /**
+ * @var TPreparedCommand SQL command prepareer
+ */
+ private $_command;
+
+ /**
+ * @var TSqlMapper sqlmap used by this mapper.
+ */
+ private $_manager;
+
+ /**
+ * @var TPostSelectBinding[] post select statement queue.
+ */
+ private $_selectQueue=array();
+
+ /**
+ * @var boolean true when data is mapped to a particular row.
+ */
+ private $_IsRowDataFound = false;
+
+ /**
+ * @var TSQLMapObjectCollectionTree group by object collection tree
+ */
+ private $_groupBy;
+
+ /**
+ * @var Post select is to query for list.
+ */
+ const QUERY_FOR_LIST = 0;
+
+ /**
+ * @var Post select is to query for list.
+ */
+ const QUERY_FOR_ARRAY = 1;
+
+ /**
+ * @var Post select is to query for object.
+ */
+ const QUERY_FOR_OBJECT = 2;
+
+ /**
+ * @return string Name used to identify the TMappedStatement amongst the others.
+ * This the name of the SQL statement by default.
+ */
+ public function getID()
+ {
+ return $this->_statement->ID;
+ }
+
+ /**
+ * @return TSqlMapStatement The SQL statment used by this MappedStatement
+ */
+ public function getStatement()
+ {
+ return $this->_statement;
+ }
+
+ /**
+ * @return TSqlMapper The SqlMap used by this MappedStatement
+ */
+ public function getManager()
+ {
+ return $this->_manager;
+ }
+
+ /**
+ * @return TPreparedCommand command to prepare SQL statements.
+ */
+ public function getCommand()
+ {
+ return $this->_command;
+ }
+
+ /**
+ * Empty the group by results cache.
+ */
+ protected function initialGroupByResults()
+ {
+ $this->_groupBy = new TSqlMapObjectCollectionTree();
+ }
+
+ /**
+ * Creates a new mapped statement.
+ * @param TSqlMapper an sqlmap.
+ * @param TSqlMapStatement An SQL statement.
+ */
+ public function __construct(TSqlMapManager $sqlMap, TSqlMapStatement $statement)
+ {
+ $this->_manager = $sqlMap;
+ $this->_statement = $statement;
+ $this->_command = new TPreparedCommand();
+ $this->initialGroupByResults();
+ }
+
+ public function getSqlString()
+ {
+ return $this->getStatement()->getSqlText()->getPreparedStatement()->getPreparedSql();
+ }
+
+ /**
+ * Execute SQL Query.
+ * @param IDbConnection database connection
+ * @param array SQL statement and parameters.
+ * @return mixed record set if applicable.
+ * @throws TSqlMapExecutionException if execution error or false record set.
+ * @throws TSqlMapQueryExecutionException if any execution error
+ */
+/* protected function executeSQLQuery($connection, $sql)
+ {
+ try
+ {
+ if(!($recordSet = $connection->execute($sql['sql'],$sql['parameters'])))
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_execution_error_no_record', $this->getID(),
+ $connection->ErrorMsg());
+ }
+ return $recordSet;
+ }
+ catch (Exception $e)
+ {
+ throw new TSqlMapQueryExecutionException($this->getStatement(), $e);
+ }
+ }*/
+
+ /**
+ * Execute SQL Query with limits.
+ * @param IDbConnection database connection
+ * @param array SQL statement and parameters.
+ * @return mixed record set if applicable.
+ * @throws TSqlMapExecutionException if execution error or false record set.
+ * @throws TSqlMapQueryExecutionException if any execution error
+ */
+ protected function executeSQLQueryLimit($connection, $command, $max, $skip)
+ {
+ if($max>-1 || $skip > -1)
+ {
+ $maxStr=$max>0?' LIMIT '.$max:'';
+ $skipStr=$skip>0?' OFFSET '.$skip:'';
+ $command->setText($command->getText().$maxStr.$skipStr);
+ }
+ $connection->setActive(true);
+ return $command->query();
+
+ /*//var_dump($command);
+ try
+ {
+ $recordSet = $connection->selectLimit($sql['sql'],$max,$skip,$sql['parameters']);
+ if(!$recordSet)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_execution_error_query_for_list',
+ $connection->ErrorMsg());
+ }
+ return $recordSet;
+ }
+ catch (Exception $e)
+ {
+ throw new TSqlMapQueryExecutionException($this->getStatement(), $e);
+ }*/
+ }
+
+ /**
+ * Executes the SQL and retuns a List of result objects.
+ * @param IDbConnection database connection
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param object result collection object.
+ * @param integer The number of rows to skip over.
+ * @param integer The maximum number of rows to return.
+ * @return array a list of result objects
+ * @param callback row delegate handler
+ * @see executeQueryForList()
+ */
+ public function executeQueryForList($connection, $parameter, $result=null, $skip=-1, $max=-1, $delegate=null)
+ {
+ $sql = $this->_command->create($this->_manager, $connection, $this->_statement, $parameter,$skip,$max);
+ return $this->runQueryForList($connection, $parameter, $sql, $result, $delegate);
+ }
+
+ /**
+ * Executes the SQL and retuns a List of result objects.
+ *
+ * This method should only be called by internal developers, consider using
+ * <tt>executeQueryForList()</tt> first.
+ *
+ * @param IDbConnection database connection
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param array SQL string and subsititution parameters.
+ * @param object result collection object.
+ * @param integer The number of rows to skip over.
+ * @param integer The maximum number of rows to return.
+ * @param callback row delegate handler
+ * @return array a list of result objects
+ * @see executeQueryForList()
+ */
+ public function runQueryForList($connection, $parameter, $sql, $result, $delegate=null)
+ {
+ $registry=$this->getManager()->getTypeHandlers();
+ $list = $result instanceof ArrayAccess ? $result :
+ $this->_statement->createInstanceOfListClass($registry);
+ $connection->setActive(true);
+ $reader = $sql->query();
+ //$reader = $this->executeSQLQueryLimit($connection, $sql, $max, $skip);
+ if($delegate!==null)
+ {
+ foreach($reader as $row)
+ {
+ $obj = $this->applyResultMap($row);
+ $param = new TResultSetListItemParameter($obj, $parameter, $list);
+ $this->raiseRowDelegate($delegate, $param);
+ }
+ }
+ else
+ {
+ //var_dump($sql,$parameter);
+ foreach($reader as $row)
+ {
+// var_dump($row);
+ $list[] = $this->applyResultMap($row);
+ }
+ }
+
+ if(!$this->_groupBy->isEmpty())
+ {
+ $list = $this->_groupBy->collect();
+ $this->initialGroupByResults();
+ }
+
+ $this->executePostSelect($connection);
+ $this->onExecuteQuery($sql);
+
+ return $list;
+ }
+
+ /**
+ * Executes the SQL and retuns all rows selected in a map that is keyed on
+ * the property named in the keyProperty parameter. The value at each key
+ * will be the value of the property specified in the valueProperty parameter.
+ * If valueProperty is null, the entire result object will be entered.
+ * @param IDbConnection database connection
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param string The property of the result object to be used as the key.
+ * @param string The property of the result object to be used as the value (or null).
+ * @param callback row delegate handler
+ * @return array An array of object containing the rows keyed by keyProperty.
+ */
+ public function executeQueryForMap($connection, $parameter, $keyProperty, $valueProperty=null, $skip=-1, $max=-1, $delegate=null)
+ {
+ $sql = $this->_command->create($this->_manager, $connection, $this->_statement, $parameter, $skip, $max);
+ return $this->runQueryForMap($connection, $parameter, $sql, $keyProperty, $valueProperty, $delegate);
+ }
+
+ /**
+ * Executes the SQL and retuns all rows selected in a map that is keyed on
+ * the property named in the keyProperty parameter. The value at each key
+ * will be the value of the property specified in the valueProperty parameter.
+ * If valueProperty is null, the entire result object will be entered.
+ *
+ * This method should only be called by internal developers, consider using
+ * <tt>executeQueryForMap()</tt> first.
+ *
+ * @param IDbConnection database connection
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param array SQL string and subsititution parameters.
+ * @param string The property of the result object to be used as the key.
+ * @param string The property of the result object to be used as the value (or null).
+ * @param callback row delegate, a callback function
+ * @return array An array of object containing the rows keyed by keyProperty.
+ * @see executeQueryForMap()
+ */
+ public function runQueryForMap($connection, $parameter, $command, $keyProperty, $valueProperty=null, $delegate=null)
+ {
+ $map = array();
+ //$recordSet = $this->executeSQLQuery($connection, $sql);
+ $connection->setActive(true);
+ $reader = $command->query();
+ if($delegate!==null)
+ {
+ //while($row = $recordSet->fetchRow())
+ foreach($reader as $row)
+ {
+ $obj = $this->applyResultMap($row);
+ $key = TPropertyAccess::get($obj, $keyProperty);
+ $value = ($valueProperty===null) ? $obj :
+ TPropertyAccess::get($obj, $valueProperty);
+ $param = new TResultSetMapItemParameter($key, $value, $parameter, $map);
+ $this->raiseRowDelegate($delegate, $param);
+ }
+ }
+ else
+ {
+ //while($row = $recordSet->fetchRow())
+ foreach($reader as $row)
+ {
+ $obj = $this->applyResultMap($row);
+ $key = TPropertyAccess::get($obj, $keyProperty);
+ $map[$key] = ($valueProperty===null) ? $obj :
+ TPropertyAccess::get($obj, $valueProperty);
+ }
+ }
+ $this->onExecuteQuery($command);
+ return $map;
+ }
+
+ /**
+ * Raises delegate handler.
+ * This method is invoked for each new list item. It is the responsibility
+ * of the handler to add the item to the list.
+ * @param object event parameter
+ */
+ protected function raiseRowDelegate($handler, $param)
+ {
+ if(is_string($handler))
+ {
+ call_user_func($handler,$this,$param);
+ }
+ else if(is_callable($handler,true))
+ {
+ // an array: 0 - object, 1 - method name/path
+ list($object,$method)=$handler;
+ if(is_string($object)) // static method call
+ call_user_func($handler,$this,$param);
+ else
+ {
+ if(($pos=strrpos($method,'.'))!==false)
+ {
+ $object=$this->getSubProperty(substr($method,0,$pos));
+ $method=substr($method,$pos+1);
+ }
+ $object->$method($this,$param);
+ }
+ }
+ else
+ throw new TInvalidDataValueException('sqlmap_invalid_delegate', $this->getID(), $handler);
+ }
+
+ /**
+ * Executes an SQL statement that returns a single row as an object of the
+ * type of the <tt>$result</tt> passed in as a parameter.
+ * @param IDbConnection database connection
+ * @param mixed The parameter data (object, arrary, primitive) used to set the parameters in the SQL
+ * @param mixed The result object.
+ * @return ${return}
+ */
+ public function executeQueryForObject($connection, $parameter, $result=null)
+ {
+ $sql = $this->_command->create($this->_manager, $connection, $this->_statement, $parameter);
+ return $this->runQueryForObject($connection, $sql, $result);
+ }
+
+ /**
+ * Executes an SQL statement that returns a single row as an object of the
+ * type of the <tt>$result</tt> passed in as a parameter.
+ *
+ * This method should only be called by internal developers, consider using
+ * <tt>executeQueryForObject()</tt> first.
+ *
+ * @param IDbConnection database connection
+ * @param array SQL string and subsititution parameters.
+ * @param object The result object.
+ * @return object the object.
+ * @see executeQueryForObject()
+ */
+ public function runQueryForObject($connection, $command, &$result)
+ {
+ $object = null;
+ $connection->setActive(true);
+ foreach($command->query() as $row)
+ $object = $this->applyResultMap($row, $result);
+
+ if(!$this->_groupBy->isEmpty())
+ {
+ $list = $this->_groupBy->collect();
+ $this->initialGroupByResults();
+ $object = $list[0];
+ }
+
+ $this->executePostSelect($connection);
+ $this->onExecuteQuery($command);
+
+ return $object;
+ }
+
+ /**
+ * Execute an insert statement. Fill the parameter object with the ouput
+ * parameters if any, also could return the insert generated key.
+ * @param IDbConnection database connection
+ * @param mixed The parameter object used to fill the statement.
+ * @return string the insert generated key.
+ */
+ public function executeInsert($connection, $parameter)
+ {
+ $generatedKey = $this->getPreGeneratedSelectKey($connection, $parameter);
+
+ $command = $this->_command->create($this->_manager, $connection, $this->_statement, $parameter);
+// var_dump($command,$parameter);
+ $result = $command->execute();
+
+ if($generatedKey===null)
+ $generatedKey = $this->getPostGeneratedSelectKey($connection, $parameter);
+
+ $this->executePostSelect($connection);
+ $this->onExecuteQuery($command);
+ return $generatedKey;
+ }
+
+ /**
+ * Gets the insert generated ID before executing an insert statement.
+ * @param IDbConnection database connection
+ * @param mixed insert statement parameter.
+ * @return string new insert ID if pre-select key statement was executed, null otherwise.
+ */
+ protected function getPreGeneratedSelectKey($connection, $parameter)
+ {
+ if($this->_statement instanceof TSqlMapInsert)
+ {
+ $selectKey = $this->_statement->getSelectKey();
+ if(($selectKey!==null) && !$selectKey->getIsAfter())
+ return $this->executeSelectKey($connection, $parameter, $selectKey);
+ }
+ }
+
+ /**
+ * Gets the inserted row ID after executing an insert statement.
+ * @param IDbConnection database connection
+ * @param mixed insert statement parameter.
+ * @return string last insert ID, null otherwise.
+ */
+ protected function getPostGeneratedSelectKey($connection, $parameter)
+ {
+ if($this->_statement instanceof TSqlMapInsert)
+ {
+ $selectKey = $this->_statement->getSelectKey();
+ if(($selectKey!==null) && $selectKey->getIsAfter())
+ return $this->executeSelectKey($connection, $parameter, $selectKey);
+ }
+ }
+
+ /**
+ * Execute the select key statement, used to obtain last insert ID.
+ * @param IDbConnection database connection
+ * @param mixed insert statement parameter
+ * @param TSqlMapSelectKey select key statement
+ * @return string last insert ID.
+ */
+ protected function executeSelectKey($connection, $parameter, $selectKey)
+ {
+ $mappedStatement = $this->getManager()->getMappedStatement($selectKey->getID());
+ $generatedKey = $mappedStatement->executeQueryForObject(
+ $connection, $parameter, null);
+ if(strlen($prop = $selectKey->getProperty()) > 0)
+ TPropertyAccess::set($parameter, $prop, $generatedKey);
+ return $generatedKey;
+ }
+
+ /**
+ * Execute an update statement. Also used for delete statement.
+ * Return the number of rows effected.
+ * @param IDbConnection database connection
+ * @param mixed The object used to set the parameters in the SQL.
+ * @return integer The number of rows effected.
+ */
+ public function executeUpdate($connection, $parameter)
+ {
+ $sql = $this->_command->create($this->getManager(),$connection, $this->_statement, $parameter);
+ $affectedRows = $sql->execute();
+ //$this->executeSQLQuery($connection, $sql);
+ $this->executePostSelect($connection);
+ $this->onExecuteQuery($sql);
+ return $affectedRows;
+ }
+
+ /**
+ * Process 'select' result properties
+ * @param IDbConnection database connection
+ */
+ protected function executePostSelect($connection)
+ {
+ while(count($this->_selectQueue))
+ {
+ $postSelect = array_shift($this->_selectQueue);
+ $method = $postSelect->getMethod();
+ $statement = $postSelect->getStatement();
+ $property = $postSelect->getResultProperty()->getProperty();
+ $keys = $postSelect->getKeys();
+ $resultObject = $postSelect->getResultObject();
+
+ if($method == self::QUERY_FOR_LIST || $method == self::QUERY_FOR_ARRAY)
+ {
+ $values = $statement->executeQueryForList($connection, $keys, null);
+
+ if($method == self::QUERY_FOR_ARRAY)
+ $values = $values->toArray();
+ TPropertyAccess::set($resultObject, $property, $values);
+ }
+ else if($method == self::QUERY_FOR_OBJECT)
+ {
+ $value = $statement->executeQueryForObject($connection, $keys, null);
+ TPropertyAccess::set($resultObject, $property, $value);
+ }
+ }
+ }
+
+ /**
+ * Raise the execute query event.
+ * @param array prepared SQL statement and subsititution parameters
+ */
+ public function onExecuteQuery($sql)
+ {
+ $this->raiseEvent('OnExecuteQuery', $this, $sql);
+ }
+
+ /**
+ * Apply result mapping.
+ * @param array a result set row retrieved from the database
+ * @param object the result object, will create if necessary.
+ * @return object the result filled with data, null if not filled.
+ */
+ protected function applyResultMap($row, &$resultObject=null)
+ {
+ if($row === false) return null;
+
+ $resultMapName = $this->_statement->getResultMap();
+ $resultClass = $this->_statement->getResultClass();
+
+ $obj=null;
+ if($this->getManager()->getResultMaps()->contains($resultMapName))
+ $obj = $this->fillResultMap($resultMapName, $row, null, $resultObject);
+ else if(strlen($resultClass) > 0)
+ $obj = $this->fillResultClass($resultClass, $row, $resultObject);
+ else
+ $obj = $this->fillDefaultResultMap(null, $row, $resultObject);
+ if(class_exists('TActiveRecord',false) && $obj instanceof TActiveRecord)
+ //Create a new clean active record.
+ $obj=TActiveRecord::createRecord(get_class($obj),$obj);
+ return $obj;
+ }
+
+ /**
+ * Fill the result using ResultClass, will creates new result object if required.
+ * @param string result object class name
+ * @param array a result set row retrieved from the database
+ * @param object the result object, will create if necessary.
+ * @return object result object filled with data
+ */
+ protected function fillResultClass($resultClass, $row, $resultObject)
+ {
+ if($resultObject===null)
+ {
+ $registry = $this->getManager()->getTypeHandlers();
+ $resultObject = $this->_statement->createInstanceOfResultClass($registry,$row);
+ }
+
+ if($resultObject instanceOf ArrayAccess)
+ return $this->fillResultArrayList($row, $resultObject);
+ else if(is_object($resultObject))
+ return $this->fillResultObjectProperty($row, $resultObject);
+ else
+ return $this->fillDefaultResultMap(null, $row, $resultObject);
+ }
+
+ /**
+ * Apply the result to a TList or an array.
+ * @param array a result set row retrieved from the database
+ * @param object result object, array or list
+ * @return object result filled with data.
+ */
+ protected function fillResultArrayList($row, $resultObject)
+ {
+ if($resultObject instanceof TList)
+ foreach($row as $v)
+ $resultObject[] = $v;
+ else
+ foreach($row as $k => $v)
+ $resultObject[$k] = $v;
+ return $resultObject;
+ }
+
+ /**
+ * Apply the result to an object.
+ * @param array a result set row retrieved from the database
+ * @param object result object, array or list
+ * @return object result filled with data.
+ */
+ protected function fillResultObjectProperty($row, $resultObject)
+ {
+ $index = 0;
+ $registry=$this->getManager()->getTypeHandlers();
+ foreach($row as $k=>$v)
+ {
+ $property = new TResultProperty;
+ if(is_string($k) && strlen($k) > 0)
+ $property->setColumn($k);
+ $property->setColumnIndex(++$index);
+ $type = gettype(TPropertyAccess::get($resultObject,$k));
+ $property->setType($type);
+ $value = $property->getPropertyValue($registry,$row);
+ TPropertyAccess::set($resultObject, $k,$value);
+ }
+ return $resultObject;
+ }
+
+ /**
+ * Fills the result object according to result mappings.
+ * @param string result map name.
+ * @param array a result set row retrieved from the database
+ * @param object result object to fill, will create new instances if required.
+ * @return object result object filled with data.
+ */
+ protected function fillResultMap($resultMapName, $row, $parentGroup=null, &$resultObject=null)
+ {
+ $resultMap = $this->getManager()->getResultMap($resultMapName);
+ $registry = $this->getManager()->getTypeHandlers();
+ $resultMap = $resultMap->resolveSubMap($registry,$row);
+
+ if($resultObject===null)
+ $resultObject = $resultMap->createInstanceOfResult($registry);
+
+ if(is_object($resultObject))
+ {
+ if(strlen($resultMap->getGroupBy()) > 0)
+ return $this->addResultMapGroupBy($resultMap, $row, $parentGroup, $resultObject);
+ else
+ foreach($resultMap->getColumns() as $property)
+ $this->setObjectProperty($resultMap, $property, $row, $resultObject);
+ }
+ else
+ {
+ $resultObject = $this->fillDefaultResultMap($resultMap, $row, $resultObject);
+ }
+ return $resultObject;
+ }
+
+ /**
+ * ResultMap with GroupBy property. Save object collection graph in a tree
+ * and collect the result later.
+ * @param TResultMap result mapping details.
+ * @param array a result set row retrieved from the database
+ * @param object the result object
+ * @return object result object.
+ */
+ protected function addResultMapGroupBy($resultMap, $row, $parent, &$resultObject)
+ {
+ $group = $this->getResultMapGroupKey($resultMap, $row);
+
+ if(empty($parent))
+ {
+ $rootObject = array('object'=>$resultObject, 'property' => null);
+ $this->_groupBy->add(null, $group, $rootObject);
+ }
+
+ foreach($resultMap->getColumns() as $property)
+ {
+ //set properties.
+ $this->setObjectProperty($resultMap, $property, $row, $resultObject);
+ $nested = $property->getResultMapping();
+
+ //nested property
+ if($this->getManager()->getResultMaps()->contains($nested))
+ {
+ $nestedMap = $this->getManager()->getResultMap($nested);
+ $groupKey = $this->getResultMapGroupKey($nestedMap, $row);
+
+ //add the node reference first
+ if(empty($parent))
+ $this->_groupBy->add($group, $groupKey, '');
+
+ //get the nested result mapping value
+ $value = $this->fillResultMap($nested, $row, $groupKey);
+
+ //add it to the object tree graph
+ $groupObject = array('object'=>$value, 'property' => $property->getProperty());
+ if(empty($parent))
+ $this->_groupBy->add($group, $groupKey, $groupObject);
+ else
+ $this->_groupBy->add($parent, $groupKey, $groupObject);
+ }
+ }
+ return $resultObject;
+ }
+
+ /**
+ * Gets the result 'group by' groupping key for each row.
+ * @param TResultMap result mapping details.
+ * @param array a result set row retrieved from the database
+ * @return string groupping key.
+ */
+ protected function getResultMapGroupKey($resultMap, $row)
+ {
+ $groupBy = $resultMap->getGroupBy();
+ if(isset($row[$groupBy]))
+ return $resultMap->getID().$row[$groupBy];
+ else
+ return $resultMap->getID().crc32(serialize($row));
+ }
+
+ /**
+ * Fill the result map using default settings. If <tt>$resultMap</tt> is null
+ * the result object returned will be guessed from <tt>$resultObject</tt>.
+ * @param TResultMap result mapping details.
+ * @param array a result set row retrieved from the database
+ * @param object the result object
+ * @return mixed the result object filled with data.
+ */
+ protected function fillDefaultResultMap($resultMap, $row, $resultObject)
+ {
+ if($resultObject===null)
+ $resultObject='';
+
+ if($resultMap!==null)
+ $result = $this->fillArrayResultMap($resultMap, $row, $resultObject);
+ else
+ $result = $row;
+
+ //if scalar result types
+ if(count($result) == 1 && ($type = gettype($resultObject))!= 'array')
+ return $this->getScalarResult($result, $type);
+ else
+ return $result;
+ }
+
+ /**
+ * Retrieve the result map as an array.
+ * @param TResultMap result mapping details.
+ * @param array a result set row retrieved from the database
+ * @param object the result object
+ * @return array array list of result objects.
+ */
+ protected function fillArrayResultMap($resultMap, $row, $resultObject)
+ {
+ $result = array();
+ $registry=$this->getManager()->getTypeHandlers();
+ foreach($resultMap->getColumns() as $column)
+ {
+ if(($column->getType()===null)
+ && ($resultObject!==null) && !is_object($resultObject))
+ $column->setType(gettype($resultObject));
+ $result[$column->getProperty()] = $column->getPropertyValue($registry,$row);
+ }
+ return $result;
+ }
+
+ /**
+ * Converts the first array value to scalar value of given type.
+ * @param array list of results
+ * @param string scalar type.
+ * @return mixed scalar value.
+ */
+ protected function getScalarResult($result, $type)
+ {
+ $scalar = array_shift($result);
+ settype($scalar, $type);
+ return $scalar;
+ }
+
+ /**
+ * Set a property of the result object with appropriate value.
+ * @param TResultMap result mapping details.
+ * @param TResultProperty the result property to fill.
+ * @param array a result set row retrieved from the database
+ * @param object the result object
+ */
+ protected function setObjectProperty($resultMap, $property, $row, &$resultObject)
+ {
+ $select = $property->getSelect();
+ $key = $property->getProperty();
+ $nested = $property->getNestedResultMap();
+ $registry=$this->getManager()->getTypeHandlers();
+ if($key === '')
+ {
+ $resultObject = $property->getPropertyValue($registry,$row);
+ }
+ else if(strlen($select) == 0 && ($nested===null))
+ {
+ $value = $property->getPropertyValue($registry,$row);
+
+ $this->_IsRowDataFound = $this->_IsRowDataFound || ($value != null);
+ if(is_array($resultObject) || is_object($resultObject))
+ TPropertyAccess::set($resultObject, $key, $value);
+ else
+ $resultObject = $value;
+ }
+ else if($nested!==null)
+ {
+ if($property->instanceOfListType($resultObject) || $property->instanceOfArrayType($resultObject))
+ {
+ if(strlen($resultMap->getGroupBy()) <= 0)
+ throw new TSqlMapExecutionException(
+ 'sqlmap_non_groupby_array_list_type', $resultMap->getID(),
+ get_class($resultObject), $key);
+ }
+ else
+ {
+ $obj = $nested->createInstanceOfResult($this->getManager()->getTypeHandlers());
+ if($this->fillPropertyWithResultMap($nested, $row, $obj) == false)
+ $obj = null;
+ TPropertyAccess::set($resultObject, $key, $obj);
+ }
+ }
+ else //'select' ResultProperty
+ {
+ $this->enquequePostSelect($select, $resultMap, $property, $row, $resultObject);
+ }
+ }
+
+ /**
+ * Add nested result property to post select queue.
+ * @param string post select statement ID
+ * @param TResultMap current result mapping details.
+ * @param TResultProperty current result property.
+ * @param array a result set row retrieved from the database
+ * @param object the result object
+ */
+ protected function enquequePostSelect($select, $resultMap, $property, $row, $resultObject)
+ {
+ $statement = $this->getManager()->getMappedStatement($select);
+ $key = $this->getPostSelectKeys($resultMap, $property, $row);
+ $postSelect = new TPostSelectBinding;
+ $postSelect->setStatement($statement);
+ $postSelect->setResultObject($resultObject);
+ $postSelect->setResultProperty($property);
+ $postSelect->setKeys($key);
+
+ if($property->instanceOfListType($resultObject))
+ {
+ $values = null;
+ if($property->getLazyLoad())
+ {
+ $values = TLazyLoadList::newInstance($statement, $key,
+ $resultObject, $property->getProperty());
+ TPropertyAccess::set($resultObject, $property->getProperty(), $values);
+ }
+ else
+ $postSelect->setMethod(self::QUERY_FOR_LIST);
+ }
+ else if($property->instanceOfArrayType($resultObject))
+ $postSelect->setMethod(self::QUERY_FOR_ARRAY);
+ else
+ $postSelect->setMethod(self::QUERY_FOR_OBJECT);
+
+ if(!$property->getLazyLoad())
+ $this->_selectQueue[] = $postSelect;
+ }
+
+ /**
+ * Finds in the post select property the SQL statement primary selection keys.
+ * @param TResultMap result mapping details
+ * @param TResultProperty result property
+ * @param array current row data.
+ * @return array list of primary key values.
+ */
+ protected function getPostSelectKeys($resultMap, $property,$row)
+ {
+ $value = $property->getColumn();
+ if(is_int(strpos($value.',',0)) || is_int(strpos($value, '=',0)))
+ {
+ $keys = array();
+ foreach(explode(',', $value) as $entry)
+ {
+ $pair =explode('=',$entry);
+ $keys[trim($pair[0])] = $row[trim($pair[1])];
+ }
+ return $keys;
+ }
+ else
+ {
+ $registry=$this->getManager()->getTypeHandlers();
+ return $property->getPropertyValue($registry,$row);
+ }
+ }
+
+ /**
+ * Fills the property with result mapping results.
+ * @param TResultMap nested result mapping details.
+ * @param array a result set row retrieved from the database
+ * @param object the result object
+ * @return boolean true if the data was found, false otherwise.
+ */
+ protected function fillPropertyWithResultMap($resultMap, $row, &$resultObject)
+ {
+ $dataFound = false;
+ foreach($resultMap->getColumns() as $property)
+ {
+ $this->_IsRowDataFound = false;
+ $this->setObjectProperty($resultMap, $property, $row, $resultObject);
+ $dataFound = $dataFound || $this->_IsRowDataFound;
+ }
+ $this->_IsRowDataFound = $dataFound;
+ return $dataFound;
+ }
+
+ public function __wakeup()
+ {
+ parent::__wakeup();
+ if (is_null($this->_selectQueue)) $this->_selectQueue = array();
+ }
+
+ public function __sleep()
+ {
+ $exprops = array(); $cn = __CLASS__;
+ if (!count($this->_selectQueue)) $exprops[] = "\0$cn\0_selectQueue";
+ if (is_null($this->_groupBy)) $exprops[] = "\0$cn\0_groupBy";
+ if (!$this->_IsRowDataFound) $exprops[] = "\0$cn\0_IsRowDataFound";
+ return array_diff(parent::__sleep(),$exprops);
+ }
+}
+
+/**
+ * TPostSelectBinding class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TPostSelectBinding
+{
+ private $_statement=null;
+ private $_property=null;
+ private $_resultObject=null;
+ private $_keys=null;
+ private $_method=TMappedStatement::QUERY_FOR_LIST;
+
+ public function getStatement(){ return $this->_statement; }
+ public function setStatement($value){ $this->_statement = $value; }
+
+ public function getResultProperty(){ return $this->_property; }
+ public function setResultProperty($value){ $this->_property = $value; }
+
+ public function getResultObject(){ return $this->_resultObject; }
+ public function setResultObject($value){ $this->_resultObject = $value; }
+
+ public function getKeys(){ return $this->_keys; }
+ public function setKeys($value){ $this->_keys = $value; }
+
+ public function getMethod(){ return $this->_method; }
+ public function setMethod($value){ $this->_method = $value; }
+}
+
+/**
+ * TSQLMapObjectCollectionTree class.
+ *
+ * Maps object collection graphs as trees. Nodes in the collection can
+ * be {@link add} using parent relationships. The object collections can be
+ * build using the {@link collect} method.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TSqlMapObjectCollectionTree extends TComponent
+{
+ /**
+ * @var array object graph as tree
+ */
+ private $_tree = array();
+ /**
+ * @var array tree node values
+ */
+ private $_entries = array();
+ /**
+ * @var array resulting object collection
+ */
+ private $_list = array();
+
+ /**
+ * @return boolean true if the graph is empty
+ */
+ public function isEmpty()
+ {
+ return count($this->_entries) == 0;
+ }
+
+ /**
+ * Add a new node to the object tree graph.
+ * @param string parent node id
+ * @param string new node id
+ * @param mixed node value
+ */
+ public function add($parent, $node, $object='')
+ {
+ if(isset($this->_entries[$parent]) && ($this->_entries[$parent]!==null)
+ && isset($this->_entries[$node]) && ($this->_entries[$node]!==null))
+ {
+ $this->_entries[$node] = $object;
+ return;
+ }
+ $this->_entries[$node] = $object;
+ if(empty($parent))
+ {
+ if(isset($this->_entries[$node]))
+ return;
+ $this->_tree[$node] = array();
+ }
+ $found = $this->addNode($this->_tree, $parent, $node);
+ if(!$found && !empty($parent))
+ {
+ $this->_tree[$parent] = array();
+ if(!isset($this->_entries[$parent]) || $object !== '')
+ $this->_entries[$parent] = $object;
+ $this->addNode($this->_tree, $parent, $node);
+ }
+ }
+
+ /**
+ * Find the parent node and add the new node as its child.
+ * @param array list of nodes to check
+ * @param string parent node id
+ * @param string new node id
+ * @return boolean true if parent node is found.
+ */
+ protected function addNode(&$childs, $parent, $node)
+ {
+ $found = false;
+ reset($childs);
+ for($i = 0, $k = count($childs); $i < $k; $i++)
+ {
+ $key = key($childs);
+ next($childs);
+ if($key == $parent)
+ {
+ $found = true;
+ $childs[$key][$node] = array();
+ }
+ else
+ {
+ $found = $found || $this->addNode($childs[$key], $parent, $node);
+ }
+ }
+ return $found;
+ }
+
+ /**
+ * @return array object collection
+ */
+ public function collect()
+ {
+ while(count($this->_tree) > 0)
+ $this->collectChildren(null, $this->_tree);
+ return $this->getCollection();
+ }
+
+ /**
+ * @param array list of nodes to check
+ * @return boolean true if all nodes are leaf nodes, false otherwise
+ */
+ protected function hasChildren(&$nodes)
+ {
+ $hasChildren = false;
+ foreach($nodes as $node)
+ if(count($node) != 0)
+ return true;
+ return $hasChildren;
+ }
+
+ /**
+ * Visit all the child nodes and collect them by removing.
+ * @param string parent node id
+ * @param array list of child nodes.
+ */
+ protected function collectChildren($parent, &$nodes)
+ {
+ $noChildren = !$this->hasChildren($nodes);
+ $childs = array();
+ for(reset($nodes); $key = key($nodes);)
+ {
+ next($nodes);
+ if($noChildren)
+ {
+ $childs[] = $key;
+ unset($nodes[$key]);
+ }
+ else
+ $this->collectChildren($key, $nodes[$key]);
+ }
+ if(count($childs) > 0)
+ $this->onChildNodesVisited($parent, $childs);
+ }
+
+ /**
+ * Set the object properties for all the child nodes visited.
+ * @param string parent node id
+ * @param array list of child nodes visited.
+ */
+ protected function onChildNodesVisited($parent, $nodes)
+ {
+ if(empty($parent) || empty($this->_entries[$parent]))
+ return;
+
+ $parentObject = $this->_entries[$parent]['object'];
+ $property = $this->_entries[$nodes[0]]['property'];
+
+ $list = TPropertyAccess::get($parentObject, $property);
+
+ foreach($nodes as $node)
+ {
+ if($list instanceof TList)
+ $parentObject->{$property}[] = $this->_entries[$node]['object'];
+ else if(is_array($list))
+ $list[] = $this->_entries[$node]['object'];
+ else
+ throw new TSqlMapExecutionException(
+ 'sqlmap_property_must_be_list');
+ }
+
+ if(is_array($list))
+ TPropertyAccess::set($parentObject, $property, $list);
+
+ if($this->_entries[$parent]['property'] === null)
+ $this->_list[] = $parentObject;
+ }
+
+ /**
+ * @return array object collection.
+ */
+ protected function getCollection()
+ {
+ return $this->_list;
+ }
+
+ public function __sleep()
+ {
+ $exprops = array(); $cn = __CLASS__;
+ if (!count($this->_tree)) $exprops[] = "\0$cn\0_tree";
+ if (!count($this->_entries)) $exprops[] = "\0$cn\0_entries";
+ if (!count($this->_list)) $exprops[] = "\0$cn\0_list";
+ return array_diff(parent::__sleep(),$exprops);
+ }
+}
+
+/**
+ * TResultSetListItemParameter class
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TResultSetListItemParameter extends TComponent
+{
+ private $_resultObject;
+ private $_parameterObject;
+ private $_list;
+
+ public function __construct($result, $parameter, &$list)
+ {
+ $this->_resultObject = $result;
+ $this->_parameterObject = $parameter;
+ $this->_list = &$list;
+ }
+
+ public function getResult()
+ {
+ return $this->_resultObject;
+ }
+
+ public function getParameter()
+ {
+ return $this->_parameterObject;
+ }
+
+ public function &getList()
+ {
+ return $this->_list;
+ }
+}
+
+/**
+ * TResultSetMapItemParameter class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TResultSetMapItemParameter extends TComponent
+{
+ private $_key;
+ private $_value;
+ private $_parameterObject;
+ private $_map;
+
+ public function __construct($key, $value, $parameter, &$map)
+ {
+ $this->_key = $key;
+ $this->_value = $value;
+ $this->_parameterObject = $parameter;
+ $this->_map = &$map;
+ }
+
+ public function getKey()
+ {
+ return $this->_key;
+ }
+
+ public function getValue()
+ {
+ return $this->_value;
+ }
+
+ public function getParameter()
+ {
+ return $this->_parameterObject;
+ }
+
+ public function &getMap()
+ {
+ return $this->_map;
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TPreparedCommand.php b/lib/prado/framework/Data/SqlMap/Statements/TPreparedCommand.php
new file mode 100644
index 0000000..1f560b3
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TPreparedCommand.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * TPreparedCommand 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.SqlMap.Statements
+ */
+
+Prado::using('System.Data.Common.TDbMetaData');
+Prado::using('System.Data.Common.TDbCommandBuilder');
+
+/**
+ * TPreparedCommand class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TPreparedCommand
+{
+ public function create(TSqlMapManager $manager, $connection, $statement, $parameterObject,$skip=null,$max=null)
+ {
+ $sqlText = $statement->getSQLText();
+
+ $prepared = $sqlText->getPreparedStatement($parameterObject);
+ $connection->setActive(true);
+ $sql = $prepared->getPreparedSql();
+
+ if($sqlText instanceof TSimpleDynamicSql)
+ $sql = $sqlText->replaceDynamicParameter($sql, $parameterObject);
+
+ if($max!==null || $skip!==null)
+ {
+ $builder = TDbMetaData::getInstance($connection)->createCommandBuilder();
+ $sql = $builder->applyLimitOffset($sql,$max,$skip);
+ }
+ $command = $connection->createCommand($sql);
+ $this->applyParameterMap($manager, $command, $prepared, $statement, $parameterObject);
+
+ return $command;
+ }
+
+ protected function applyParameterMap($manager,$command,$prepared, $statement, $parameterObject)
+ {
+ $properties = $prepared->getParameterNames(false);
+ //$parameters = $prepared->getParameterValues();
+ $registry=$manager->getTypeHandlers();
+ if ($properties)
+ for($i = 0, $k=$properties->getCount(); $i<$k; $i++)
+ {
+ $property = $statement->parameterMap()->getProperty($i);
+ $value = $statement->parameterMap()->getPropertyValue($registry,$property, $parameterObject);
+ $dbType = $property->getDbType();
+ if($dbType=='') //relies on PHP lax comparison
+ $command->bindValue($i+1,$value, TDbCommandBuilder::getPdoType($value));
+ else if(strpos($dbType, 'PDO::')===0)
+ $command->bindValue($i+1,$value, constant($property->getDbType())); //assumes PDO types, e.g. PDO::PARAM_INT
+ else
+ $command->bindValue($i+1,$value);
+ }
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TPreparedStatement.php b/lib/prado/framework/Data/SqlMap/Statements/TPreparedStatement.php
new file mode 100644
index 0000000..f536e39
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TPreparedStatement.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * TPreparedStatement 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.SqlMap.Statements
+ */
+
+/**
+ * TpreparedStatement class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TPreparedStatement extends TComponent
+{
+ private $_sqlString='';
+ private $_parameterNames;
+ private $_parameterValues;
+
+ public function getPreparedSql(){ return $this->_sqlString; }
+ public function setPreparedSql($value){ $this->_sqlString = $value; }
+
+ public function getParameterNames($needed = true)
+ {
+ if (!$this->_parameterNames and $needed)
+ $this->_parameterNames = new TList;
+ return $this->_parameterNames;
+ }
+
+ public function setParameterNames($value){ $this->_parameterNames = $value; }
+
+ public function getParameterValues($needed = true)
+ {
+ if (!$this->_parameterValues and $needed)
+ $this->_parameterValues=new TMap;
+ return $this->_parameterValues;
+ }
+
+ public function setParameterValues($value){ $this->_parameterValues = $value; }
+
+ public function __sleep()
+ {
+ $exprops = array(); $cn = __CLASS__;
+ if (!$this->_parameterNames or !$this->_parameterNames->getCount()) $exprops[] = "\0$cn\0_parameterNames";
+ if (!$this->_parameterValues or !$this->_parameterValues->getCount()) $exprops[] = "\0$cn\0_parameterValues";
+ return array_diff(parent::__sleep(),$exprops);
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TPreparedStatementFactory.php b/lib/prado/framework/Data/SqlMap/Statements/TPreparedStatementFactory.php
new file mode 100644
index 0000000..a85cd76
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TPreparedStatementFactory.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * TPreparedStatementFactory 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.SqlMap.Statements
+ */
+
+/**
+ * TPreparedStatementFactory class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TPreparedStatementFactory
+{
+ private $_statement;
+ private $_preparedStatement;
+ private $_parameterPrefix = 'param';
+ private $_commandText;
+
+ public function __construct($statement, $sqlString)
+ {
+ $this->_statement = $statement;
+ $this->_commandText = $sqlString;
+ }
+
+ public function prepare()
+ {
+ $this->_preparedStatement = new TPreparedStatement();
+ $this->_preparedStatement->setPreparedSql($this->_commandText);
+ if($this->_statement->parameterMap()!==null)
+ $this->createParametersForTextCommand();
+ return $this->_preparedStatement;
+ }
+
+ protected function createParametersForTextCommand()
+ {
+ foreach($this->_statement->ParameterMap()->getProperties() as $prop)
+ $this->_preparedStatement->getParameterNames()->add($prop->getProperty());
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TSelectMappedStatement.php b/lib/prado/framework/Data/SqlMap/Statements/TSelectMappedStatement.php
new file mode 100644
index 0000000..0231c09
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TSelectMappedStatement.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * TSelectMappedStatement 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.SqlMap.Statements
+ */
+
+/**
+ * TSelectMappedStatment class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TSelectMappedStatement extends TMappedStatement
+{
+ public function executeInsert($connection, $parameter)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_insert', get_class($this), $this->getID());
+ }
+
+ public function executeUpdate($connection, $parameter)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_update', get_class($this), $this->getID());
+ }
+
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TSimpleDynamicSql.php b/lib/prado/framework/Data/SqlMap/Statements/TSimpleDynamicSql.php
new file mode 100644
index 0000000..11f8a56
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TSimpleDynamicSql.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * TSimpleDynamicSql 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.SqlMap.Statements
+ */
+
+/**
+ * TSimpleDynamicSql class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TSimpleDynamicSql extends TStaticSql
+{
+ private $_mappings=array();
+
+ public function __construct($mappings)
+ {
+ $this->_mappings = $mappings;
+ }
+
+ public function replaceDynamicParameter($sql, $parameter)
+ {
+ foreach($this->_mappings as $property)
+ {
+ $value = TPropertyAccess::get($parameter, $property);
+ $sql = preg_replace('/'.TSimpleDynamicParser::DYNAMIC_TOKEN.'/', str_replace('$', '\$', $value), $sql, 1);
+ }
+ return $sql;
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TStaticSql.php b/lib/prado/framework/Data/SqlMap/Statements/TStaticSql.php
new file mode 100644
index 0000000..180d0e4
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TStaticSql.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * TStaticSql 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.SqlMap.Statements
+ */
+
+/**
+ * TStaticSql class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TStaticSql extends TComponent
+{
+ private $_preparedStatement;
+
+ public function buildPreparedStatement($statement, $sqlString)
+ {
+ $factory = new TPreparedStatementFactory($statement, $sqlString);
+ $this->_preparedStatement = $factory->prepare();
+ }
+
+ public function getPreparedStatement($parameter=null)
+ {
+ return $this->_preparedStatement;
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/Statements/TUpdateMappedStatement.php b/lib/prado/framework/Data/SqlMap/Statements/TUpdateMappedStatement.php
new file mode 100644
index 0000000..8a39640
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/Statements/TUpdateMappedStatement.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * TUpdateMappedStatement 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.SqlMap.Statements
+ */
+
+/**
+ * TUpdateMappedStatement class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap.Statements
+ * @since 3.1
+ */
+class TUpdateMappedStatement extends TMappedStatement
+{
+ public function executeInsert($connection, $parameter)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_insert', get_class($this), $this->getID());
+ }
+
+ public function executeQueryForMap($connection, $parameter, $keyProperty,
+ $valueProperty=null)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_query_for_map', get_class($this), $this->getID());
+ }
+
+ public function executeQueryForList($connection, $parameter, $result=null,
+ $skip=-1, $max=-1)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_query_for_list', get_class($this), $this->getID());
+ }
+
+ public function executeQueryForObject($connection, $parameter, $result=null)
+ {
+ throw new TSqlMapExecutionException(
+ 'sqlmap_cannot_execute_query_for_object', get_class($this), $this->getID());
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/TSqlMapConfig.php b/lib/prado/framework/Data/SqlMap/TSqlMapConfig.php
new file mode 100644
index 0000000..5fa641a
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/TSqlMapConfig.php
@@ -0,0 +1,179 @@
+<?php
+/**
+ * TSqlMapConfig 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.SqlMap
+ */
+
+Prado::using('System.Data.TDataSourceConfig');
+
+/**
+ * TSqlMapConfig module configuration class.
+ *
+ * Database connection and TSqlMapManager configuration.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapConfig extends TDataSourceConfig
+{
+ private $_configFile;
+ private $_sqlmap;
+ private $_enableCache=false;
+
+ /**
+ * File extension of external configuration file
+ */
+ const CONFIG_FILE_EXT='.xml';
+
+ /**
+ * @return string module ID + configuration file path.
+ */
+ private function getCacheKey()
+ {
+ return $this->getID().$this->getConfigFile();
+ }
+
+ /**
+ * Deletes the configuration cache.
+ */
+ public function clearCache()
+ {
+ $cache = $this->getApplication()->getCache();
+ if($cache !== null) {
+ $cache->delete($this->getCacheKey());
+ }
+ }
+
+ /**
+ * Create and configure the data mapper using sqlmap configuration file.
+ * Or if cache is enabled and manager already cached load from cache.
+ * If cache is enabled, the data mapper instance is cached.
+ *
+ * @return TSqlMapManager SqlMap manager instance
+ * @since 3.1.7
+ */
+ public function getSqlMapManager() {
+ Prado::using('System.Data.SqlMap.TSqlMapManager');
+ if(($manager = $this->loadCachedSqlMapManager())===null)
+ {
+ $manager = new TSqlMapManager($this->getDbConnection());
+ if(strlen($file=$this->getConfigFile()) > 0)
+ {
+ $manager->configureXml($file);
+ $this->cacheSqlMapManager($manager);
+ }
+ }
+ elseif($this->getConnectionID() !== '') {
+ $manager->setDbConnection($this->getDbConnection());
+ }
+ return $manager;
+ }
+
+ /**
+ * Saves the current SqlMap manager to cache.
+ * @return boolean true if SqlMap manager was cached, false otherwise.
+ */
+ protected function cacheSqlMapManager($manager)
+ {
+ if($this->getEnableCache())
+ {
+ $cache = $this->getApplication()->getCache();
+ if($cache !== null) {
+ $dependencies = null;
+ if($this->getApplication()->getMode() !== TApplicationMode::Performance)
+ $dependencies = $manager->getCacheDependencies();
+ return $cache->set($this->getCacheKey(), $manager, 0, $dependencies);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Loads SqlMap manager from cache.
+ * @return TSqlMapManager SqlMap manager intance if load was successful, null otherwise.
+ */
+ protected function loadCachedSqlMapManager()
+ {
+ if($this->getEnableCache())
+ {
+ $cache = $this->getApplication()->getCache();
+ if($cache !== null)
+ {
+ $manager = $cache->get($this->getCacheKey());
+ if($manager instanceof TSqlMapManager)
+ return $manager;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return string SqlMap configuration file.
+ */
+ public function getConfigFile()
+ {
+ return $this->_configFile;
+ }
+
+ /**
+ * @param string external configuration file in namespace format. The file
+ * extension must be '.xml'.
+ * @throws TConfigurationException if the file is invalid.
+ */
+ public function setConfigFile($value)
+ {
+ if(is_file($value))
+ $this->_configFile=$value;
+ else
+ {
+ $file = Prado::getPathOfNamespace($value,self::CONFIG_FILE_EXT);
+ if($file === null || !is_file($file))
+ throw new TConfigurationException('sqlmap_configfile_invalid',$value);
+ else
+ $this->_configFile = $file;
+ }
+ }
+
+ /**
+ * Set true to cache sqlmap instances.
+ * @param boolean true to cache sqlmap instance.
+ */
+ public function setEnableCache($value)
+ {
+ $this->_enableCache = TPropertyValue::ensureBoolean($value);
+ }
+
+ /**
+ * @return boolean true if configuration should be cached, false otherwise.
+ */
+ public function getEnableCache()
+ {
+ return $this->_enableCache;
+ }
+
+ /**
+ * @return TSqlMapGateway SqlMap gateway instance.
+ */
+ protected function createSqlMapGateway()
+ {
+ return $this->getSqlMapManager()->getSqlmapGateway();
+ }
+
+ /**
+ * Initialize the sqlmap if necessary, returns the TSqlMapGateway instance.
+ * @return TSqlMapGateway SqlMap gateway instance.
+ */
+ public function getClient()
+ {
+ if($this->_sqlmap===null )
+ $this->_sqlmap=$this->createSqlMapGateway();
+ return $this->_sqlmap;
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/TSqlMapGateway.php b/lib/prado/framework/Data/SqlMap/TSqlMapGateway.php
new file mode 100644
index 0000000..e1df2e5
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/TSqlMapGateway.php
@@ -0,0 +1,259 @@
+<?php
+/**
+ * TSqlMapGateway 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.SqlMap
+ */
+
+Prado::using('System.Data.SqlMap.TSqlMapManager');
+
+/**
+ * DataMapper client, a fascade to provide access the rest of the DataMapper
+ * framework. It provides three core functions:
+ *
+ * # execute an update query (including insert and delete).
+ * # execute a select query for a single object
+ * # execute a select query for a list of objects
+ *
+ * This class should be instantiated from a TSqlMapManager instance.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapGateway extends TComponent
+{
+ /**
+ * @var TSqlMapManager manager
+ */
+ private $_manager;
+
+ public function __construct($manager)
+ {
+ $this->_manager=$manager;
+ }
+
+ /**
+ * @return TSqlMapManager sqlmap manager.
+ */
+ public function getSqlMapManager()
+ {
+ return $this->_manager;
+ }
+
+ /**
+ * @return TDbConnection database connection.
+ */
+ public function getDbConnection()
+ {
+ return $this->getSqlMapManager()->getDbConnection();
+ }
+
+ /**
+ * Executes a Sql SELECT statement that returns that returns data
+ * to populate a single object instance.
+ *
+ * The parameter object is generally used to supply the input
+ * data for the WHERE clause parameter(s) of the SELECT statement.
+ *
+ * @param string The name of the sql statement to execute.
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param mixed An object of the type to be returned.
+ * @return object A single result object populated with the result set data.
+ */
+ public function queryForObject($statementName, $parameter=null, $result=null)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return $statement->executeQueryForObject($this->getDbConnection(), $parameter, $result);
+ }
+
+ /**
+ * Executes a Sql SELECT statement that returns data to populate a number
+ * of result objects.
+ *
+ * The parameter object is generally used to supply the input
+ * data for the WHERE clause parameter(s) of the SELECT statement.
+ *
+ * @param string The name of the sql statement to execute.
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param TList An Ilist object used to hold the objects,
+ * pass in null if want to return a list instead.
+ * @param int The number of rows to skip over.
+ * @param int The maximum number of rows to return.
+ * @return TList A List of result objects.
+ */
+ public function queryForList($statementName, $parameter=null, $result=null, $skip=-1, $max=-1)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return $statement->executeQueryForList($this->getDbConnection(),$parameter, $result, $skip, $max);
+ }
+
+ /**
+ * Runs a query for list with a custom object that gets a chance to deal
+ * with each row as it is processed.
+ *
+ * Example: $sqlmap->queryWithRowDelegate('getAccounts', array($this, 'rowHandler'));
+ *
+ * @param string The name of the sql statement to execute.
+ * @param callback Row delegate handler, a valid callback required.
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param TList An Ilist object used to hold the objects,
+ * pass in null if want to return a list instead.
+ * @param int The number of rows to skip over.
+ * @param int The maximum number of rows to return.
+ * @return TList A List of result objects.
+ */
+ public function queryWithRowDelegate($statementName, $delegate, $parameter=null, $result=null, $skip=-1, $max=-1)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return $statement->executeQueryForList($this->getDbConnection(), $parameter, $result, $skip, $max, $delegate);
+ }
+
+ /**
+ * Executes the SQL and retuns a subset of the results in a dynamic
+ * TPagedList that can be used to automatically scroll through results
+ * from a database table.
+ * @param string The name of the sql statement to execute.
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param integer The maximum number of objects to store in each page.
+ * @param integer The number of the page to initially load into the list.
+ * @return TPagedList A PaginatedList of beans containing the rows.
+ */
+ public function queryForPagedList($statementName, $parameter=null, $pageSize=10, $page=0)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return new TSqlMapPagedList($statement, $parameter, $pageSize, null, $page);
+ }
+
+ /**
+ * Executes the SQL and retuns a subset of the results in a dynamic
+ * TPagedList that can be used to automatically scroll through results
+ * from a database table.
+ *
+ * Runs paged list query with row delegate
+ * Example: $sqlmap->queryForPagedListWithRowDelegate('getAccounts', array($this, 'rowHandler'));
+ *
+ * @param string The name of the sql statement to execute.
+ * @param callback Row delegate handler, a valid callback required.
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param integer The maximum number of objects to store in each page.
+ * @param integer The number of the page to initially load into the list.
+ * @return TPagedList A PaginatedList of beans containing the rows.
+ */
+ public function queryForPagedListWithRowDelegate($statementName,$delegate, $parameter=null, $pageSize=10, $page=0)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return new TSqlMapPagedList($statement, $parameter, $pageSize, $delegate,$page);
+ }
+
+
+ /**
+ * Executes the SQL and retuns all rows selected in a map that is keyed on
+ * the property named in the keyProperty parameter. The value at each key
+ * will be the value of the property specified in the valueProperty
+ * parameter. If valueProperty is null, the entire result object will be
+ * entered.
+ * @param string The name of the sql statement to execute.
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param string The property of the result object to be used as the key.
+ * @param string The property of the result object to be used as the value.
+ * @return TMap Array object containing the rows keyed by keyProperty.
+ */
+ public function queryForMap($statementName, $parameter=null, $keyProperty=null, $valueProperty=null, $skip=-1, $max=-1)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return $statement->executeQueryForMap($this->getDbConnection(), $parameter, $keyProperty, $valueProperty, $skip, $max);
+ }
+
+ /**
+ * Runs a query with a custom object that gets a chance to deal
+ * with each row as it is processed.
+ *
+ * Example: $sqlmap->queryForMapWithRowDelegate('getAccounts', array($this, 'rowHandler'));
+ *
+ * @param string The name of the sql statement to execute.
+ * @param callback Row delegate handler, a valid callback required.
+ * @param mixed The object used to set the parameters in the SQL.
+ * @param string The property of the result object to be used as the key.
+ * @param string The property of the result object to be used as the value.
+ * @return TMap Array object containing the rows keyed by keyProperty.
+ */
+ public function queryForMapWithRowDelegate($statementName, $delegate, $parameter=null, $keyProperty=null, $valueProperty=null, $skip=-1, $max=-1)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return $statement->executeQueryForMap($this->getDbConnection(), $parameter, $keyProperty, $valueProperty, $skip, $max, $delegate);
+ }
+
+ /**
+ * Executes a Sql INSERT statement.
+ *
+ * Insert is a bit different from other update methods, as it provides
+ * facilities for returning the primary key of the newly inserted row
+ * (rather than the effected rows),
+ *
+ * The parameter object is generally used to supply the input data for the
+ * INSERT values.
+ *
+ * @param string The name of the statement to execute.
+ * @param string The parameter object.
+ * @return mixed The primary key of the newly inserted row.
+ * This might be automatically generated by the RDBMS,
+ * or selected from a sequence table or other source.
+ */
+ public function insert($statementName, $parameter=null)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return $statement->executeInsert($this->getDbConnection(), $parameter);
+ }
+
+ /**
+ * Executes a Sql UPDATE statement.
+ *
+ * Update can also be used for any other update statement type, such as
+ * inserts and deletes. Update returns the number of rows effected.
+ *
+ * The parameter object is generally used to supply the input data for the
+ * UPDATE values as well as the WHERE clause parameter(s).
+ *
+ * @param string The name of the statement to execute.
+ * @param mixed The parameter object.
+ * @return integer The number of rows effected.
+ */
+ public function update($statementName, $parameter=null)
+ {
+ $statement = $this->getSqlMapManager()->getMappedStatement($statementName);
+ return $statement->executeUpdate($this->getDbConnection(), $parameter);
+ }
+
+ /**
+ * Executes a Sql DELETE statement. Delete returns the number of rows effected.
+ * @param string The name of the statement to execute.
+ * @param mixed The parameter object.
+ * @return integer The number of rows effected.
+ */
+ public function delete($statementName, $parameter=null)
+ {
+ return $this->update($statementName, $parameter);
+ }
+
+ /**
+ * Flushes all cached objects that belong to this SqlMap
+ */
+ public function flushCaches()
+ {
+ $this->getSqlMapManager()->flushCacheModels();
+ }
+
+ /**
+ * @param TSqlMapTypeHandler new type handler.
+ */
+ public function registerTypeHandler($typeHandler)
+ {
+ $this->getSqlMapManager()->getTypeHandlers()->registerTypeHandler($typeHandler);
+ }
+}
+
diff --git a/lib/prado/framework/Data/SqlMap/TSqlMapManager.php b/lib/prado/framework/Data/SqlMap/TSqlMapManager.php
new file mode 100644
index 0000000..eba42f7
--- /dev/null
+++ b/lib/prado/framework/Data/SqlMap/TSqlMapManager.php
@@ -0,0 +1,272 @@
+<?php
+/**
+ * TSqlMapManager 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.SqlMap
+ */
+
+Prado::using('System.Data.SqlMap.TSqlMapGateway');
+Prado::using('System.Data.SqlMap.DataMapper.TSqlMapException');
+Prado::using('System.Data.SqlMap.DataMapper.TSqlMapTypeHandlerRegistry');
+Prado::using('System.Data.SqlMap.DataMapper.TSqlMapCache');
+Prado::using('System.Data.SqlMap.Configuration.TSqlMapStatement');
+Prado::using('System.Data.SqlMap.Configuration.*');
+Prado::using('System.Data.SqlMap.DataMapper.*');
+Prado::using('System.Data.SqlMap.Statements.*');
+Prado::using('System.Caching.TCache');
+
+
+/**
+ * TSqlMapManager class holds the sqlmap configuation result maps, statements
+ * parameter maps and a type handler factory.
+ *
+ * Use {@link SqlMapGateway getSqlMapGateway()} property to obtain the gateway
+ * instance used for querying statements defined in the SqlMap configuration files.
+ *
+ * <code>
+ * $conn = new TDbConnection($dsn,$dbuser,$dbpass);
+ * $manager = new TSqlMapManager($conn);
+ * $manager->configureXml('mydb-sqlmap.xml');
+ * $sqlmap = $manager->getSqlMapGateway();
+ * $result = $sqlmap->queryForObject('Products');
+ * </code>
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @package System.Data.SqlMap
+ * @since 3.1
+ */
+class TSqlMapManager extends TComponent
+{
+ private $_mappedStatements;
+ private $_resultMaps;
+ private $_parameterMaps;
+ private $_typeHandlers;
+ private $_cacheModels;
+
+ private $_connection;
+ private $_gateway;
+ private $_cacheDependencies;
+
+ /**
+ * Constructor, create a new SqlMap manager.
+ * @param TDbConnection database connection
+ * @param string configuration file.
+ */
+ public function __construct($connection=null)
+ {
+ $this->_connection=$connection;
+
+ $this->_mappedStatements=new TMap;
+ $this->_resultMaps=new TMap;
+ $this->_parameterMaps=new TMap;
+ $this->_cacheModels=new TMap;
+ }
+
+ /**
+ * @param TDbConnection default database connection
+ */
+ public function setDbConnection($conn)
+ {
+ $this->_connection=$conn;
+ }
+
+ /**
+ * @return TDbConnection default database connection
+ */
+ public function getDbConnection()
+ {
+ return $this->_connection;
+ }
+
+ /**
+ * @return TTypeHandlerFactory The TypeHandlerFactory
+ */
+ public function getTypeHandlers()
+ {
+ if($this->_typeHandlers===null)
+ $this->_typeHandlers= new TSqlMapTypeHandlerRegistry();
+ return $this->_typeHandlers;
+ }
+
+ /**
+ * @return TSqlMapGateway SqlMap gateway.
+ */
+ public function getSqlmapGateway()
+ {
+ if($this->_gateway===null)
+ $this->_gateway=$this->createSqlMapGateway();
+ return $this->_gateway;
+ }
+
+ /**
+ * Loads and parses the SqlMap configuration file.
+ * @param string xml configuration file.
+ */
+ public function configureXml($file)
+ {
+ $config = new TSqlMapXmlConfiguration($this);
+ $config->configure($file);
+ }
+
+ /**
+ * @return TChainedCacheDependency
+ * @since 3.1.5
+ */
+ public function getCacheDependencies()
+ {
+ if($this->_cacheDependencies === null)
+ $this->_cacheDependencies=new TChainedCacheDependency();
+
+ return $this->_cacheDependencies;
+ }
+
+ /**
+ * Configures the current TSqlMapManager using the given xml configuration file
+ * defined in {@link ConfigFile setConfigFile()}.
+ * @return TSqlMapGateway create and configure a new TSqlMapGateway.
+ */
+ protected function createSqlMapGateway()
+ {
+ return new TSqlMapGateway($this);
+ }
+
+ /**
+ * @return TMap mapped statements collection.
+ */
+ public function getMappedStatements()
+ {
+ return $this->_mappedStatements;
+ }
+
+ /**
+ * Gets a MappedStatement by name.
+ * @param string The name of the statement.
+ * @return IMappedStatement The MappedStatement
+ * @throws TSqlMapUndefinedException
+ */
+ public function getMappedStatement($name)
+ {
+ if($this->_mappedStatements->contains($name) == false)
+ throw new TSqlMapUndefinedException('sqlmap_contains_no_statement', $name);
+ return $this->_mappedStatements[$name];
+ }
+
+ /**
+ * Adds a (named) MappedStatement.
+ * @param string The key name
+ * @param IMappedStatement The statement to add
+ * @throws TSqlMapDuplicateException
+ */
+ public function addMappedStatement(IMappedStatement $statement)
+ {
+ $key = $statement->getID();
+ if($this->_mappedStatements->contains($key) == true)
+ throw new TSqlMapDuplicateException('sqlmap_already_contains_statement', $key);
+ $this->_mappedStatements->add($key, $statement);
+ }
+
+ /**
+ * @return TMap result maps collection.
+ */
+ public function getResultMaps()
+ {
+ return $this->_resultMaps;
+ }
+
+ /**
+ * Gets a named result map
+ * @param string result name.
+ * @return TResultMap the result map.
+ * @throws TSqlMapUndefinedException
+ */
+ public function getResultMap($name)
+ {
+ if($this->_resultMaps->contains($name) == false)
+ throw new TSqlMapUndefinedException('sqlmap_contains_no_result_map', $name);
+ return $this->_resultMaps[$name];
+ }
+
+ /**
+ * @param TResultMap add a new result map to this SQLMap
+ * @throws TSqlMapDuplicateException
+ */
+ public function addResultMap(TResultMap $result)
+ {
+ $key = $result->getID();
+ if($this->_resultMaps->contains($key) == true)
+ throw new TSqlMapDuplicateException('sqlmap_already_contains_result_map', $key);
+ $this->_resultMaps->add($key, $result);
+ }
+
+ /**
+ * @return TMap parameter maps collection.
+ */
+ public function getParameterMaps()
+ {
+ return $this->_parameterMaps;
+ }
+
+ /**
+ * @param string parameter map ID name.
+ * @return TParameterMap the parameter with given ID.
+ * @throws TSqlMapUndefinedException
+ */
+ public function getParameterMap($name)
+ {
+ if($this->_parameterMaps->contains($name) == false)
+ throw new TSqlMapUndefinedException('sqlmap_contains_no_parameter_map', $name);
+ return $this->_parameterMaps[$name];
+ }
+
+ /**
+ * @param TParameterMap add a new parameter map to this SQLMap.
+ * @throws TSqlMapDuplicateException
+ */
+ public function addParameterMap(TParameterMap $parameter)
+ {
+ $key = $parameter->getID();
+ if($this->_parameterMaps->contains($key) == true)
+ throw new TSqlMapDuplicateException('sqlmap_already_contains_parameter_map', $key);
+ $this->_parameterMaps->add($key, $parameter);
+ }
+
+ /**
+ * Adds a named cache.
+ * @param TSqlMapCacheModel the cache to add.
+ * @throws TSqlMapConfigurationException
+ */
+ public function addCacheModel(TSqlMapCacheModel $cacheModel)
+ {
+ if($this->_cacheModels->contains($cacheModel->getID()))
+ throw new TSqlMapConfigurationException('sqlmap_cache_model_already_exists', $cacheModel->getID());
+ else
+ $this->_cacheModels->add($cacheModel->getID(), $cacheModel);
+ }
+
+ /**
+ * Gets a cache by name
+ * @param string the name of the cache to get.
+ * @return TSqlMapCacheModel the cache object.
+ * @throws TSqlMapConfigurationException
+ */
+ public function getCacheModel($name)
+ {
+ if(!$this->_cacheModels->contains($name))
+ throw new TSqlMapConfigurationException('sqlmap_unable_to_find_cache_model', $name);
+ return $this->_cacheModels[$name];
+ }
+
+ /**
+ * Flushes all cached objects that belong to this SqlMap
+ */
+ public function flushCacheModels()
+ {
+ foreach($this->_cacheModels as $cache)
+ $cache->flush();
+ }
+}
+
diff --git a/lib/prado/framework/Data/TDataSourceConfig.php b/lib/prado/framework/Data/TDataSourceConfig.php
new file mode 100644
index 0000000..e9c00da
--- /dev/null
+++ b/lib/prado/framework/Data/TDataSourceConfig.php
@@ -0,0 +1,165 @@
+<?php
+/**
+ * TDataSourceConfig 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
+ */
+
+Prado::using('System.Data.TDbConnection');
+
+/**
+ * TDataSourceConfig module class provides <module> configuration for database connections.
+ *
+ * Example usage: mysql connection
+ * <code>
+ * <modules>
+ * <module id="db1">
+ * <database ConnectionString="mysqli:host=localhost;dbname=test"
+ * username="dbuser" password="dbpass" />
+ * </module>
+ * </modules>
+ * </code>
+ *
+ * Usage in php:
+ * <code>
+ * class Home extends TPage
+ * {
+ * function onLoad($param)
+ * {
+ * $db = $this->Application->Modules['db1']->DbConnection;
+ * $db->createCommand('...'); //...
+ * }
+ * }
+ * </code>
+ *
+ * The properties of <connection> are those of the class TDbConnection.
+ * Set {@link setConnectionClass} attribute for a custom database connection class
+ * that extends the TDbConnection class.
+ *
+ * @author Wei Zhuo <weizho[at]gmail[dot]com>
+ * @package System.Data
+ * @since 3.1
+ */
+class TDataSourceConfig extends TModule
+{
+ private $_connID='';
+ private $_conn;
+ private $_connClass='System.Data.TDbConnection';
+
+ /**
+ * Initalize the database connection properties from attributes in <database> tag.
+ * @param TXmlDocument xml configuration.
+ */
+ public function init($xml)
+ {
+ if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP)
+ {
+ if(isset($xml['database']) && is_array($xml['database']))
+ {
+ $db=$this->getDbConnection();
+ foreach($xml['database'] as $name=>$value)
+ $db->setSubProperty($name,$value);
+ }
+ }
+ else
+ {
+ if($prop=$xml->getElementByTagName('database'))
+ {
+ $db=$this->getDbConnection();
+ foreach($prop->getAttributes() as $name=>$value)
+ $db->setSubproperty($name,$value);
+ }
+ }
+ }
+
+ /**
+ * The module ID of another TDataSourceConfig. The {@link getDbConnection DbConnection}
+ * property of this configuration will equal to {@link getDbConnection DbConnection}
+ * of the given TDataSourceConfig module.
+ * @param string module ID.
+ */
+ public function setConnectionID($value)
+ {
+ $this->_connID=$value;
+ }
+
+ /**
+ * @return string connection module ID.
+ */
+ public function getConnectionID()
+ {
+ return $this->_connID;
+ }
+
+ /**
+ * Gets the TDbConnection from another module if {@link setConnectionID ConnectionID}
+ * is supplied and valid. Otherwise, a connection of type given by
+ * {@link setConnectionClass ConnectionClass} is created.
+ * @return TDbConnection database connection.
+ */
+ public function getDbConnection()
+ {
+ if($this->_conn===null)
+ {
+ if($this->_connID!=='')
+ $this->_conn = $this->findConnectionByID($this->getConnectionID());
+ else
+ $this->_conn = Prado::createComponent($this->getConnectionClass());
+ }
+ return $this->_conn;
+ }
+
+ /**
+ * Alias for getDbConnection().
+ * @return TDbConnection database connection.
+ */
+ public function getDatabase()
+ {
+ return $this->getDbConnection();
+ }
+
+ /**
+ * @param string Database connection class name to be created.
+ */
+ public function getConnectionClass()
+ {
+ return $this->_connClass;
+ }
+
+ /**
+ * The database connection class name to be created when {@link getDbConnection}
+ * method is called <b>and</b> {@link setConnectionID ConnectionID} is null. The
+ * {@link setConnectionClass ConnectionClass} property must be set before
+ * calling {@link getDbConnection} if you wish to create the connection using the
+ * given class name.
+ * @param string Database connection class name.
+ * @throws TConfigurationException when database connection is already established.
+ */
+ public function setConnectionClass($value)
+ {
+ if($this->_conn!==null)
+ throw new TConfigurationException('datasource_dbconnection_exists', $value);
+ $this->_connClass=$value;
+ }
+
+ /**
+ * Finds the database connection instance from the Application modules.
+ * @param string Database connection module ID.
+ * @return TDbConnection database connection.
+ * @throws TConfigurationException when module is not of TDbConnection or TDataSourceConfig.
+ */
+ protected function findConnectionByID($id)
+ {
+ $conn = $this->getApplication()->getModule($id);
+ if($conn instanceof TDbConnection)
+ return $conn;
+ else if($conn instanceof TDataSourceConfig)
+ return $conn->getDbConnection();
+ else
+ throw new TConfigurationException('datasource_dbconnection_invalid',$id);
+ }
+}
diff --git a/lib/prado/framework/Data/TDbCommand.php b/lib/prado/framework/Data/TDbCommand.php
new file mode 100644
index 0000000..93e9e74
--- /dev/null
+++ b/lib/prado/framework/Data/TDbCommand.php
@@ -0,0 +1,306 @@
+<?php
+/**
+ * TDbCommand class file
+ *
+ * @author Qiang Xue <qiang.xue@gmail.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
+ */
+
+/**
+ * TDbCommand class.
+ *
+ * TDbCommand represents an SQL statement to execute against a database.
+ * It is usually created by calling {@link TDbConnection::createCommand}.
+ * The SQL statement to be executed may be set via {@link setText Text}.
+ *
+ * To execute a non-query SQL (such as insert, delete, update), call
+ * {@link execute}. To execute an SQL statement that returns result data set
+ * (such as select), use {@link query} or its convenient versions {@link queryRow}
+ * and {@link queryScalar}.
+ *
+ * If an SQL statement returns results (such as a SELECT SQL), the results
+ * can be accessed via the returned {@link TDbDataReader}.
+ *
+ * TDbCommand supports SQL statment preparation and parameter binding.
+ * Call {@link bindParameter} to bind a PHP variable to a parameter in SQL.
+ * Call {@link bindValue} to bind a value to an SQL parameter.
+ * When binding a parameter, the SQL statement is automatically prepared.
+ * You may also call {@link prepare} to explicitly prepare an SQL statement.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @package System.Data
+ * @since 3.0
+ */
+class TDbCommand extends TComponent
+{
+ private $_connection;
+ private $_text='';
+ private $_statement=null;
+
+ /**
+ * Constructor.
+ * @param TDbConnection the database connection
+ * @param string the SQL statement to be executed
+ */
+ public function __construct(TDbConnection $connection,$text)
+ {
+ $this->_connection=$connection;
+ $this->setText($text);
+ }
+
+ /**
+ * Set the statement to null when serializing.
+ */
+ public function __sleep()
+ {
+ return array_diff(parent::__sleep(),array("\0TDbCommand\0_statement"));
+ }
+
+ /**
+ * @return string the SQL statement to be executed
+ */
+ public function getText()
+ {
+ return $this->_text;
+ }
+
+ /**
+ * Specifies the SQL statement to be executed.
+ * Any previous execution will be terminated or cancel.
+ * @param string the SQL statement to be executed
+ */
+ public function setText($value)
+ {
+ $this->_text=$value;
+ $this->cancel();
+ }
+
+ /**
+ * @return TDbConnection the connection associated with this command
+ */
+ public function getConnection()
+ {
+ return $this->_connection;
+ }
+
+ /**
+ * @return PDOStatement the underlying PDOStatement for this command
+ * It could be null if the statement is not prepared yet.
+ */
+ public function getPdoStatement()
+ {
+ return $this->_statement;
+ }
+
+ /**
+ * Prepares the SQL statement to be executed.
+ * For complex SQL statement that is to be executed multiple times,
+ * this may improve performance.
+ * For SQL statement with binding parameters, this method is invoked
+ * automatically.
+ */
+ public function prepare()
+ {
+ if($this->_statement==null)
+ {
+ try
+ {
+ $this->_statement=$this->getConnection()->getPdoInstance()->prepare($this->getText());
+ }
+ catch(Exception $e)
+ {
+ throw new TDbException('dbcommand_prepare_failed',$e->getMessage(),$this->getText());
+ }
+ }
+ }
+
+ /**
+ * Cancels the execution of the SQL statement.
+ */
+ public function cancel()
+ {
+ $this->_statement=null;
+ }
+
+ /**
+ * Binds a parameter to the SQL statement to be executed.
+ * @param mixed Parameter identifier. For a prepared statement
+ * using named placeholders, this will be a parameter name of
+ * the form :name. For a prepared statement using question mark
+ * placeholders, this will be the 1-indexed position of the parameter.
+ * Unlike {@link bindValue}, the variable is bound as a reference and will
+ * only be evaluated at the time that {@link execute} or {@link query} is called.
+ * @param mixed Name of the PHP variable to bind to the SQL statement parameter
+ * @param int SQL data type of the parameter
+ * @param int length of the data type
+ * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php
+ */
+ public function bindParameter($name, &$value, $dataType=null, $length=null)
+ {
+ $this->prepare();
+ if($dataType===null)
+ $this->_statement->bindParam($name,$value);
+ else if($length===null)
+ $this->_statement->bindParam($name,$value,$dataType);
+ else
+ $this->_statement->bindParam($name,$value,$dataType,$length);
+ }
+
+ /**
+ * Binds a value to a parameter.
+ * @param mixed Parameter identifier. For a prepared statement
+ * using named placeholders, this will be a parameter name of
+ * the form :name. For a prepared statement using question mark
+ * placeholders, this will be the 1-indexed position of the parameter.
+ * @param mixed The value to bind to the parameter
+ * @param int SQL data type of the parameter
+ * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php
+ */
+ public function bindValue($name, $value, $dataType=null)
+ {
+ $this->prepare();
+ if($dataType===null)
+ $this->_statement->bindValue($name,$value);
+ else
+ $this->_statement->bindValue($name,$value,$dataType);
+ }
+
+ /**
+ * Executes the SQL statement.
+ * This method is meant only for executing non-query SQL statement.
+ * No result set will be returned.
+ * @return integer number of rows affected by the execution.
+ * @throws TDbException execution failed
+ */
+ public function execute()
+ {
+ try
+ {
+ // Do not trace because it will remain even in
+ // Performance mode or when pradolite.php is used
+ // Prado::trace('Execute Command: '.$this->getDebugStatementText(), 'System.Data');
+ if($this->_statement instanceof PDOStatement)
+ {
+ $this->_statement->execute();
+ return $this->_statement->rowCount();
+ }
+ else
+ return $this->getConnection()->getPdoInstance()->exec($this->getText());
+ }
+ catch(Exception $e)
+ {
+ throw new TDbException('dbcommand_execute_failed',$e->getMessage(),$this->getDebugStatementText());
+ }
+ }
+
+ /**
+ * @return String prepared SQL text for debugging purposes.
+ */
+ public function getDebugStatementText()
+ {
+ if(Prado::getApplication()->getMode() === TApplicationMode::Debug)
+ return $this->_statement instanceof PDOStatement ?
+ $this->_statement->queryString
+ : $this->getText();
+ }
+
+ /**
+ * Executes the SQL statement and returns query result.
+ * This method is for executing an SQL query that returns result set.
+ * @return TDbDataReader the reader object for fetching the query result
+ * @throws TDbException execution failed
+ */
+ public function query()
+ {
+ try
+ {
+ // Prado::trace('Query: '.$this->getDebugStatementText(), 'System.Data');
+ if($this->_statement instanceof PDOStatement)
+ $this->_statement->execute();
+ else
+ $this->_statement=$this->getConnection()->getPdoInstance()->query($this->getText());
+ return new TDbDataReader($this);
+ }
+ catch(Exception $e)
+ {
+ throw new TDbException('dbcommand_query_failed',$e->getMessage(),$this->getDebugStatementText());
+ }
+ }
+
+ /**
+ * Executes the SQL statement and returns the first row of the result.
+ * This is a convenient method of {@link query} when only the first row of data is needed.
+ * @param boolean whether the row should be returned as an associated array with
+ * column names as the keys or the array keys are column indexes (0-based).
+ * @return array the first row of the query result, false if no result.
+ * @throws TDbException execution failed
+ */
+ public function queryRow($fetchAssociative=true)
+ {
+ try
+ {
+ // Prado::trace('Query Row: '.$this->getDebugStatementText(), 'System.Data');
+ if($this->_statement instanceof PDOStatement)
+ $this->_statement->execute();
+ else
+ $this->_statement=$this->getConnection()->getPdoInstance()->query($this->getText());
+ $result=$this->_statement->fetch($fetchAssociative ? PDO::FETCH_ASSOC : PDO::FETCH_NUM);
+ $this->_statement->closeCursor();
+ return $result;
+ }
+ catch(Exception $e)
+ {
+ throw new TDbException('dbcommand_query_failed',$e->getMessage(),$this->getDebugStatementText());
+ }
+ }
+
+ /**
+ * Executes the SQL statement and returns the value of the first column in the first row of data.
+ * This is a convenient method of {@link query} when only a single scalar
+ * value is needed (e.g. obtaining the count of the records).
+ * @return mixed the value of the first column in the first row of the query result. False is returned if there is no value.
+ * @throws TDbException execution failed
+ */
+ public function queryScalar()
+ {
+ try
+ {
+ // Prado::trace('Query Scalar: '.$this->getDebugStatementText(), 'System.Data');
+ if($this->_statement instanceof PDOStatement)
+ $this->_statement->execute();
+ else
+ $this->_statement=$this->getConnection()->getPdoInstance()->query($this->getText());
+ $result=$this->_statement->fetchColumn();
+ $this->_statement->closeCursor();
+ if(is_resource($result) && get_resource_type($result)==='stream')
+ return stream_get_contents($result);
+ else
+ return $result;
+ }
+ catch(Exception $e)
+ {
+ throw new TDbException('dbcommand_query_failed',$e->getMessage(),$this->getDebugStatementText());
+ }
+ }
+
+ /**
+ * Executes the SQL statement and returns the first column of the result.
+ * This is a convenient method of {@link query} when only the first column of data is needed.
+ * Note, the column returned will contain the first element in each row of result.
+ * @return array the first column of the query result. Empty array if no result.
+ * @throws TDbException execution failed
+ * @since 3.1.2
+ */
+ public function queryColumn()
+ {
+ $rows=$this->query()->readAll();
+ $column=array();
+ foreach($rows as $row)
+ $column[]=current($row);
+ return $column;
+ }
+}
+
diff --git a/lib/prado/framework/Data/TDbConnection.php b/lib/prado/framework/Data/TDbConnection.php
new file mode 100644
index 0000000..d9c43f9
--- /dev/null
+++ b/lib/prado/framework/Data/TDbConnection.php
@@ -0,0 +1,680 @@
+<?php
+/**
+ * TDbConnection class file
+ *
+ * @author Qiang Xue <qiang.xue@gmail.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
+ */
+
+Prado::using('System.Data.TDbTransaction');
+Prado::using('System.Data.TDbCommand');
+
+/**
+ * TDbConnection class
+ *
+ * TDbConnection represents a connection to a database.
+ *
+ * TDbConnection works together with {@link TDbCommand}, {@link TDbDataReader}
+ * and {@link TDbTransaction} to provide data access to various DBMS
+ * in a common set of APIs. They are a thin wrapper of the {@link http://www.php.net/manual/en/ref.pdo.php PDO}
+ * PHP extension.
+ *
+ * To establish a connection, set {@link setActive Active} to true after
+ * specifying {@link setConnectionString ConnectionString}, {@link setUsername Username}
+ * and {@link setPassword Password}.
+ *
+ * Since 3.1.2, the connection charset can be set (for MySQL and PostgreSQL databases only)
+ * using the {@link setCharset Charset} property. The value of this property is database dependant.
+ * e.g. for mysql, you can use 'latin1' for cp1252 West European, 'utf8' for unicode, ...
+ *
+ * The following example shows how to create a TDbConnection instance and establish
+ * the actual connection:
+ * <code>
+ * $connection=new TDbConnection($dsn,$username,$password);
+ * $connection->Active=true;
+ * </code>
+ *
+ * After the DB connection is established, one can execute an SQL statement like the following:
+ * <code>
+ * $command=$connection->createCommand($sqlStatement);
+ * $command->execute(); // a non-query SQL statement execution
+ * // or execute an SQL query and fetch the result set
+ * $reader=$command->query();
+ *
+ * // each $row is an array representing a row of data
+ * foreach($reader as $row) ...
+ * </code>
+ *
+ * One can do prepared SQL execution and bind parameters to the prepared SQL:
+ * <code>
+ * $command=$connection->createCommand($sqlStatement);
+ * $command->bindParameter($name1,$value1);
+ * $command->bindParameter($name2,$value2);
+ * $command->execute();
+ * </code>
+ *
+ * To use transaction, do like the following:
+ * <code>
+ * $transaction=$connection->beginTransaction();
+ * try
+ * {
+ * $connection->createCommand($sql1)->execute();
+ * $connection->createCommand($sql2)->execute();
+ * //.... other SQL executions
+ * $transaction->commit();
+ * }
+ * catch(Exception $e)
+ * {
+ * $transaction->rollBack();
+ * }
+ * </code>
+ *
+ * TDbConnection provides a set of methods to support setting and querying
+ * of certain DBMS attributes, such as {@link getNullConversion NullConversion}.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @package System.Data
+ * @since 3.0
+ */
+class TDbConnection extends TComponent
+{
+ /**
+ *
+ * @since 3.1.7
+ */
+ const DEFAULT_TRANSACTION_CLASS = 'System.Data.TDbTransaction';
+
+ private $_dsn='';
+ private $_username='';
+ private $_password='';
+ private $_charset='';
+ private $_attributes=array();
+ private $_active=false;
+ private $_pdo=null;
+ private $_transaction;
+
+ /**
+ * @var TDbMetaData
+ */
+ private $_dbMeta = null;
+
+ /**
+ * @var string
+ * @since 3.1.7
+ */
+ private $_transactionClass=self::DEFAULT_TRANSACTION_CLASS;
+
+ /**
+ * Constructor.
+ * Note, the DB connection is not established when this connection
+ * instance is created. Set {@link setActive Active} property to true
+ * to establish the connection.
+ * Since 3.1.2, you can set the charset for MySql connection
+ *
+ * @param string The Data Source Name, or DSN, contains the information required to connect to the database.
+ * @param string The user name for the DSN string.
+ * @param string The password for the DSN string.
+ * @param string Charset used for DB Connection (MySql & pgsql only). If not set, will use the default charset of your database server
+ * @see http://www.php.net/manual/en/function.PDO-construct.php
+ */
+ public function __construct($dsn='',$username='',$password='', $charset='')
+ {
+ $this->_dsn=$dsn;
+ $this->_username=$username;
+ $this->_password=$password;
+ $this->_charset=$charset;
+ }
+
+ /**
+ * Close the connection when serializing.
+ */
+ public function __sleep()
+ {
+// $this->close(); - DO NOT CLOSE the current connection as serializing doesn't neccessarily mean we don't this connection anymore in the current session
+ return array_diff(parent::__sleep(),array("\0TDbConnection\0_pdo","\0TDbConnection\0_active"));
+ }
+
+ /**
+ * @return array list of available PDO drivers
+ * @see http://www.php.net/manual/en/function.PDO-getAvailableDrivers.php
+ */
+ public static function getAvailableDrivers()
+ {
+ return PDO::getAvailableDrivers();
+ }
+
+ /**
+ * @return boolean whether the DB connection is established
+ */
+ public function getActive()
+ {
+ return $this->_active;
+ }
+
+ /**
+ * Open or close the DB connection.
+ * @param boolean whether to open or close DB connection
+ * @throws TDbException if connection fails
+ */
+ public function setActive($value)
+ {
+ $value=TPropertyValue::ensureBoolean($value);
+ if($value!==$this->_active)
+ {
+ if($value)
+ $this->open();
+ else
+ $this->close();
+ }
+ }
+
+ /**
+ * Opens DB connection if it is currently not
+ * @throws TDbException if connection fails
+ */
+ protected function open()
+ {
+ if($this->_pdo===null)
+ {
+ try
+ {
+ $this->_pdo=new PDO($this->getConnectionString(),$this->getUsername(),
+ $this->getPassword(),$this->_attributes);
+ // This attribute is only useful for PDO::MySql driver.
+ // Ignore the warning if a driver doesn't understand this.
+ @$this->_pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
+ $this->_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ $this->_active=true;
+ $this->setConnectionCharset();
+ }
+ catch(PDOException $e)
+ {
+ throw new TDbException('dbconnection_open_failed',$e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * Closes the currently active DB connection.
+ * It does nothing if the connection is already closed.
+ */
+ protected function close()
+ {
+ $this->_pdo=null;
+ $this->_active=false;
+ }
+
+ /*
+ * Set the database connection charset.
+ * Only MySql databases are supported for now.
+ * @since 3.1.2
+ */
+ protected function setConnectionCharset()
+ {
+ if ($this->_charset === '' || $this->_active === false)
+ return;
+ switch ($this->_pdo->getAttribute(PDO::ATTR_DRIVER_NAME))
+ {
+ case 'mysql':
+ case 'sqlite':
+ $stmt = $this->_pdo->prepare('SET NAMES ?');
+ break;
+ case 'pgsql':
+ $stmt = $this->_pdo->prepare('SET client_encoding TO ?');
+ break;
+ default:
+ throw new TDbException('dbconnection_unsupported_driver_charset', $driver);
+ }
+ $stmt->execute(array($this->_charset));
+ }
+
+ /**
+ * @return string The Data Source Name, or DSN, contains the information required to connect to the database.
+ */
+ public function getConnectionString()
+ {
+ return $this->_dsn;
+ }
+
+ /**
+ * @param string The Data Source Name, or DSN, contains the information required to connect to the database.
+ * @see http://www.php.net/manual/en/function.PDO-construct.php
+ */
+ public function setConnectionString($value)
+ {
+ $this->_dsn=$value;
+ }
+
+ /**
+ * @return string the username for establishing DB connection. Defaults to empty string.
+ */
+ public function getUsername()
+ {
+ return $this->_username;
+ }
+
+ /**
+ * @param string the username for establishing DB connection
+ */
+ public function setUsername($value)
+ {
+ $this->_username=$value;
+ }
+
+ /**
+ * @return string the password for establishing DB connection. Defaults to empty string.
+ */
+ public function getPassword()
+ {
+ return $this->_password;
+ }
+
+ /**
+ * @param string the password for establishing DB connection
+ */
+ public function setPassword($value)
+ {
+ $this->_password=$value;
+ }
+
+ /**
+ * @return string the charset used for database connection. Defaults to emtpy string.
+ */
+ public function getCharset ()
+ {
+ return $this->_charset;
+ }
+
+ /**
+ * @param string the charset used for database connection
+ */
+ public function setCharset ($value)
+ {
+ $this->_charset=$value;
+ $this->setConnectionCharset();
+ }
+
+ /**
+ * @return PDO the PDO instance, null if the connection is not established yet
+ */
+ public function getPdoInstance()
+ {
+ return $this->_pdo;
+ }
+
+ /**
+ * Creates a command for execution.
+ * @param string SQL statement associated with the new command.
+ * @return TDbCommand the DB command
+ * @throws TDbException if the connection is not active
+ */
+ public function createCommand($sql)
+ {
+ if($this->getActive())
+ return new TDbCommand($this,$sql);
+ else
+ throw new TDbException('dbconnection_connection_inactive');
+ }
+
+ /**
+ * @return TDbTransaction the currently active transaction. Null if no active transaction.
+ */
+ public function getCurrentTransaction()
+ {
+ if($this->_transaction!==null)
+ {
+ if($this->_transaction->getActive())
+ return $this->_transaction;
+ }
+ return null;
+ }
+
+ /**
+ * Starts a transaction.
+ * @return TDbTransaction the transaction initiated
+ * @throws TDbException if the connection is not active
+ */
+ public function beginTransaction()
+ {
+ if($this->getActive())
+ {
+ $this->_pdo->beginTransaction();
+ return $this->_transaction=Prado::createComponent($this->getTransactionClass(), $this);
+ }
+ else
+ throw new TDbException('dbconnection_connection_inactive');
+ }
+
+ /**
+ * @return string Transaction class name to be created by calling {@link TDbConnection::beginTransaction}. Defaults to 'System.Data.TDbTransaction'.
+ * @since 3.1.7
+ */
+ public function getTransactionClass()
+ {
+ return $this->_transactionClass;
+ }
+
+
+ /**
+ * @param string Transaction class name to be created by calling {@link TDbConnection::beginTransaction}.
+ * @since 3.1.7
+ */
+ public function setTransactionClass($value)
+ {
+ $this->_transactionClass = (string)$value;
+ }
+
+ /**
+ * Returns the ID of the last inserted row or sequence value.
+ * @param string name of the sequence object (required by some DBMS)
+ * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
+ * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
+ */
+ public function getLastInsertID($sequenceName='')
+ {
+ if($this->getActive())
+ return $this->_pdo->lastInsertId($sequenceName);
+ else
+ throw new TDbException('dbconnection_connection_inactive');
+ }
+
+ /**
+ * Quotes a string for use in a query.
+ * @param string string to be quoted
+ * @return string the properly quoted string
+ * @see http://www.php.net/manual/en/function.PDO-quote.php
+ */
+ public function quoteString($str)
+ {
+ if($this->getActive())
+ return $this->_pdo->quote($str);
+ else
+ throw new TDbException('dbconnection_connection_inactive');
+ }
+
+ /**
+ * Quotes a table name for use in a query.
+ * @param string $name table name
+ * @return string the properly quoted table name
+ */
+ public function quoteTableName($name)
+ {
+ return $this->getDbMetaData()->quoteTableName($name);
+ }
+
+ /**
+ * Quotes a column name for use in a query.
+ * @param string $name column name
+ * @return string the properly quoted column name
+ */
+ public function quoteColumnName($name)
+ {
+ return $this->getDbMetaData()->quoteColumnName($name);
+ }
+
+ /**
+ * Quotes a column alias for use in a query.
+ * @param string $name column name
+ * @return string the properly quoted column alias
+ */
+ public function quoteColumnAlias($name)
+ {
+ return $this->getDbMetaData()->quoteColumnAlias($name);
+ }
+
+ /**
+ * @return TDbMetaData
+ */
+ public function getDbMetaData()
+ {
+ if($this->_dbMeta===null)
+ {
+ Prado::using('System.Data.Common.TDbMetaData');
+ $this->_dbMeta = TDbMetaData::getInstance($this);
+ }
+ return $this->_dbMeta;
+ }
+
+ /**
+ * @return TDbColumnCaseMode the case of the column names
+ */
+ public function getColumnCase()
+ {
+ switch($this->getAttribute(PDO::ATTR_CASE))
+ {
+ case PDO::CASE_NATURAL:
+ return TDbColumnCaseMode::Preserved;
+ case PDO::CASE_LOWER:
+ return TDbColumnCaseMode::LowerCase;
+ case PDO::CASE_UPPER:
+ return TDbColumnCaseMode::UpperCase;
+ }
+ }
+
+ /**
+ * @param TDbColumnCaseMode the case of the column names
+ */
+ public function setColumnCase($value)
+ {
+ switch(TPropertyValue::ensureEnum($value,'TDbColumnCaseMode'))
+ {
+ case TDbColumnCaseMode::Preserved:
+ $value=PDO::CASE_NATURAL;
+ break;
+ case TDbColumnCaseMode::LowerCase:
+ $value=PDO::CASE_LOWER;
+ break;
+ case TDbColumnCaseMode::UpperCase:
+ $value=PDO::CASE_UPPER;
+ break;
+ }
+ $this->setAttribute(PDO::ATTR_CASE,$value);
+ }
+
+ /**
+ * @return TDbNullConversionMode how the null and empty strings are converted
+ */
+ public function getNullConversion()
+ {
+ switch($this->getAttribute(PDO::ATTR_ORACLE_NULLS))
+ {
+ case PDO::NULL_NATURAL:
+ return TDbNullConversionMode::Preserved;
+ case PDO::NULL_EMPTY_STRING:
+ return TDbNullConversionMode::EmptyStringToNull;
+ case PDO::NULL_TO_STRING:
+ return TDbNullConversionMode::NullToEmptyString;
+ }
+ }
+
+ /**
+ * @param TDbNullConversionMode how the null and empty strings are converted
+ */
+ public function setNullConversion($value)
+ {
+ switch(TPropertyValue::ensureEnum($value,'TDbNullConversionMode'))
+ {
+ case TDbNullConversionMode::Preserved:
+ $value=PDO::NULL_NATURAL;
+ break;
+ case TDbNullConversionMode::EmptyStringToNull:
+ $value=PDO::NULL_EMPTY_STRING;
+ break;
+ case TDbNullConversionMode::NullToEmptyString:
+ $value=PDO::NULL_TO_STRING;
+ break;
+ }
+ $this->setAttribute(PDO::ATTR_ORACLE_NULLS,$value);
+ }
+
+ /**
+ * @return boolean whether creating or updating a DB record will be automatically committed.
+ * Some DBMS (such as sqlite) may not support this feature.
+ */
+ public function getAutoCommit()
+ {
+ return $this->getAttribute(PDO::ATTR_AUTOCOMMIT);
+ }
+
+ /**
+ * @param boolean whether creating or updating a DB record will be automatically committed.
+ * Some DBMS (such as sqlite) may not support this feature.
+ */
+ public function setAutoCommit($value)
+ {
+ $this->setAttribute(PDO::ATTR_AUTOCOMMIT,TPropertyValue::ensureBoolean($value));
+ }
+
+ /**
+ * @return boolean whether the connection is persistent or not
+ * Some DBMS (such as sqlite) may not support this feature.
+ */
+ public function getPersistent()
+ {
+ return $this->getAttribute(PDO::ATTR_PERSISTENT);
+ }
+
+ /**
+ * @param boolean whether the connection is persistent or not
+ * Some DBMS (such as sqlite) may not support this feature.
+ */
+ public function setPersistent($value)
+ {
+ return $this->setAttribute(PDO::ATTR_PERSISTENT,TPropertyValue::ensureBoolean($value));
+ }
+
+ /**
+ * @return string name of the DB driver
+ */
+ public function getDriverName()
+ {
+ return $this->getAttribute(PDO::ATTR_DRIVER_NAME);
+ }
+
+ /**
+ * @return string the version information of the DB driver
+ */
+ public function getClientVersion()
+ {
+ return $this->getAttribute(PDO::ATTR_CLIENT_VERSION);
+ }
+
+ /**
+ * @return string the status of the connection
+ * Some DBMS (such as sqlite) may not support this feature.
+ */
+ public function getConnectionStatus()
+ {
+ return $this->getAttribute(PDO::ATTR_CONNECTION_STATUS);
+ }
+
+ /**
+ * @return boolean whether the connection performs data prefetching
+ */
+ public function getPrefetch()
+ {
+ return $this->getAttribute(PDO::ATTR_PREFETCH);
+ }
+
+ /**
+ * @return string the information of DBMS server
+ */
+ public function getServerInfo()
+ {
+ return $this->getAttribute(PDO::ATTR_SERVER_INFO);
+ }
+
+ /**
+ * @return string the version information of DBMS server
+ */
+ public function getServerVersion()
+ {
+ return $this->getAttribute(PDO::ATTR_SERVER_VERSION);
+ }
+
+ /**
+ * @return int timeout settings for the connection
+ */
+ public function getTimeout()
+ {
+ return $this->getAttribute(PDO::ATTR_TIMEOUT);
+ }
+
+ /**
+ * Obtains a specific DB connection attribute information.
+ * @param int the attribute to be queried
+ * @return mixed the corresponding attribute information
+ * @see http://www.php.net/manual/en/function.PDO-getAttribute.php
+ */
+ public function getAttribute($name)
+ {
+ if($this->getActive())
+ return $this->_pdo->getAttribute($name);
+ else
+ throw new TDbException('dbconnection_connection_inactive');
+ }
+
+ /**
+ * Sets an attribute on the database connection.
+ * @param int the attribute to be set
+ * @param mixed the attribute value
+ * @see http://www.php.net/manual/en/function.PDO-setAttribute.php
+ */
+ public function setAttribute($name,$value)
+ {
+ if($this->_pdo instanceof PDO)
+ $this->_pdo->setAttribute($name,$value);
+ else
+ $this->_attributes[$name]=$value;
+ }
+}
+
+/**
+ * TDbColumnCaseMode
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @package System.Data
+ * @since 3.0
+ */
+class TDbColumnCaseMode extends TEnumerable
+{
+ /**
+ * Column name cases are kept as is from the database
+ */
+ const Preserved='Preserved';
+ /**
+ * Column names are converted to lower case
+ */
+ const LowerCase='LowerCase';
+ /**
+ * Column names are converted to upper case
+ */
+ const UpperCase='UpperCase';
+}
+
+/**
+ * TDbNullConversionMode
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @package System.Data
+ * @since 3.0
+ */
+class TDbNullConversionMode extends TEnumerable
+{
+ /**
+ * No conversion is performed for null and empty values.
+ */
+ const Preserved='Preserved';
+ /**
+ * NULL is converted to empty string
+ */
+ const NullToEmptyString='NullToEmptyString';
+ /**
+ * Empty string is converted to NULL
+ */
+ const EmptyStringToNull='EmptyStringToNull';
+}
+
diff --git a/lib/prado/framework/Data/TDbDataReader.php b/lib/prado/framework/Data/TDbDataReader.php
new file mode 100644
index 0000000..a74952e
--- /dev/null
+++ b/lib/prado/framework/Data/TDbDataReader.php
@@ -0,0 +1,223 @@
+<?php
+/**
+ * TDbDataReader class file
+ *
+ * @author Qiang Xue <qiang.xue@gmail.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
+ */
+
+/**
+ * TDbDataReader class.
+ *
+ * TDbDataReader represents a forward-only stream of rows from a query result set.
+ *
+ * To read the current row of data, call {@link read}. The method {@link readAll}
+ * returns all the rows in a single array.
+ *
+ * One can also retrieve the rows of data in TDbDataReader by using foreach:
+ * <code>
+ * foreach($reader as $row)
+ * // $row represents a row of data
+ * </code>
+ * Since TDbDataReader is a forward-only stream, you can only traverse it once.
+ *
+ * It is possible to use a specific mode of data fetching by setting
+ * {@link setFetchMode FetchMode}. See {@link http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php}
+ * for more details.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @package System.Data
+ * @since 3.0
+ */
+class TDbDataReader extends TComponent implements Iterator
+{
+ private $_statement;
+ private $_closed=false;
+ private $_row;
+ private $_index=-1;
+
+ /**
+ * Constructor.
+ * @param TDbCommand the command generating the query result
+ */
+ public function __construct(TDbCommand $command)
+ {
+ $this->_statement=$command->getPdoStatement();
+ $this->_statement->setFetchMode(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Binds a column to a PHP variable.
+ * When rows of data are being fetched, the corresponding column value
+ * will be set in the variable. Note, the fetch mode must include PDO::FETCH_BOUND.
+ * @param mixed Number of the column (1-indexed) or name of the column
+ * in the result set. If using the column name, be aware that the name
+ * should match the case of the column, as returned by the driver.
+ * @param mixed Name of the PHP variable to which the column will be bound.
+ * @param int Data type of the parameter
+ * @see http://www.php.net/manual/en/function.PDOStatement-bindColumn.php
+ */
+ public function bindColumn($column, &$value, $dataType=null)
+ {
+ if($dataType===null)
+ $this->_statement->bindColumn($column,$value);
+ else
+ $this->_statement->bindColumn($column,$value,$dataType);
+ }
+
+ /**
+ * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php
+ */
+ public function setFetchMode($mode)
+ {
+ $params=func_get_args();
+ call_user_func_array(array($this->_statement,'setFetchMode'),$params);
+ }
+
+ /**
+ * Advances the reader to the next row in a result set.
+ * @return array|false the current row, false if no more row available
+ */
+ public function read()
+ {
+ return $this->_statement->fetch();
+ }
+
+ /**
+ * Returns a single column from the next row of a result set.
+ * @param int zero-based column index
+ * @return mixed|false the column of the current row, false if no more row available
+ */
+ public function readColumn($columnIndex)
+ {
+ return $this->_statement->fetchColumn($columnIndex);
+ }
+
+ /**
+ * Returns a single column from the next row of a result set.
+ * @param string class name of the object to be created and populated
+ * @param array list of column names whose values are to be passed as parameters in the constructor of the class being created
+ * @return mixed|false the populated object, false if no more row of data available
+ */
+ public function readObject($className,$fields)
+ {
+ return $this->_statement->fetchObject($className,$fields);
+ }
+
+ /**
+ * Reads the whole result set into an array.
+ * @return array the result set (each array element represents a row of data).
+ * An empty array will be returned if the result contains no row.
+ */
+ public function readAll()
+ {
+ return $this->_statement->fetchAll();
+ }
+
+ /**
+ * Advances the reader to the next result when reading the results of a batch of statements.
+ * This method is only useful when there are multiple result sets
+ * returned by the query. Not all DBMS support this feature.
+ */
+ public function nextResult()
+ {
+ return $this->_statement->nextRowset();
+ }
+
+ /**
+ * Closes the reader.
+ * Any further data reading will result in an exception.
+ */
+ public function close()
+ {
+ $this->_statement->closeCursor();
+ $this->_closed=true;
+ }
+
+ /**
+ * @return boolean whether the reader is closed or not.
+ */
+ public function getIsClosed()
+ {
+ return $this->_closed;
+ }
+
+ /**
+ * @return int number of rows contained in the result.
+ * Note, most DBMS may not give a meaningful count.
+ * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows.
+ */
+ public function getRowCount()
+ {
+ return $this->_statement->rowCount();
+ }
+
+ /**
+ * @return int the number of columns in the result set.
+ * Note, even there's no row in the reader, this still gives correct column number.
+ */
+ public function getColumnCount()
+ {
+ return $this->_statement->columnCount();
+ }
+
+ /**
+ * Resets the iterator to the initial state.
+ * This method is required by the interface Iterator.
+ * @throws TDbException if this method is invoked twice
+ */
+ public function rewind()
+ {
+ if($this->_index<0)
+ {
+ $this->_row=$this->_statement->fetch();
+ $this->_index=0;
+ }
+ else
+ throw new TDbException('dbdatareader_rewind_invalid');
+ }
+
+ /**
+ * Returns the index of the current row.
+ * This method is required by the interface Iterator.
+ * @return integer the index of the current row.
+ */
+ public function key()
+ {
+ return $this->_index;
+ }
+
+ /**
+ * Returns the current row.
+ * This method is required by the interface Iterator.
+ * @return mixed the current row.
+ */
+ public function current()
+ {
+ return $this->_row;
+ }
+
+ /**
+ * Moves the internal pointer to the next row.
+ * This method is required by the interface Iterator.
+ */
+ public function next()
+ {
+ $this->_row=$this->_statement->fetch();
+ $this->_index++;
+ }
+
+ /**
+ * Returns whether there is a row of data at current position.
+ * This method is required by the interface Iterator.
+ * @return boolean whether there is a row of data at current position.
+ */
+ public function valid()
+ {
+ return $this->_row!==false;
+ }
+}
+
diff --git a/lib/prado/framework/Data/TDbTransaction.php b/lib/prado/framework/Data/TDbTransaction.php
new file mode 100644
index 0000000..a7484eb
--- /dev/null
+++ b/lib/prado/framework/Data/TDbTransaction.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * TDbTransaction class file
+ *
+ * @author Qiang Xue <qiang.xue@gmail.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
+ */
+
+Prado::using('System.Data.TDbDataReader');
+
+/**
+ * TDbTransaction class.
+ *
+ * TDbTransaction represents a DB transaction.
+ * It is usually created by calling {@link TDbConnection::beginTransaction}.
+ *
+ * The following code is a common scenario of using transactions:
+ * <code>
+ * try
+ * {
+ * $transaction=$connection->beginTransaction();
+ * $connection->createCommand($sql1)->execute();
+ * $connection->createCommand($sql2)->execute();
+ * //.... other SQL executions
+ * $transaction->commit();
+ * }
+ * catch(Exception $e)
+ * {
+ * $transaction->rollBack();
+ * }
+ * </code>
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @package System.Data
+ * @since 3.0
+ */
+class TDbTransaction extends TComponent
+{
+ private $_connection=null;
+ private $_active;
+
+ /**
+ * Constructor.
+ * @param TDbConnection the connection associated with this transaction
+ * @see TDbConnection::beginTransaction
+ */
+ public function __construct(TDbConnection $connection)
+ {
+ $this->_connection=$connection;
+ $this->setActive(true);
+ }
+
+ /**
+ * Commits a transaction.
+ * @throws TDbException if the transaction or the DB connection is not active.
+ */
+ public function commit()
+ {
+ if($this->_active && $this->_connection->getActive())
+ {
+ $this->_connection->getPdoInstance()->commit();
+ $this->_active=false;
+ }
+ else
+ throw new TDbException('dbtransaction_transaction_inactive');
+ }
+
+ /**
+ * Rolls back a transaction.
+ * @throws TDbException if the transaction or the DB connection is not active.
+ */
+ public function rollback()
+ {
+ if($this->_active && $this->_connection->getActive())
+ {
+ $this->_connection->getPdoInstance()->rollBack();
+ $this->_active=false;
+ }
+ else
+ throw new TDbException('dbtransaction_transaction_inactive');
+ }
+
+ /**
+ * @return TDbConnection the DB connection for this transaction
+ */
+ public function getConnection()
+ {
+ return $this->_connection;
+ }
+
+ /**
+ * @return boolean whether this transaction is active
+ */
+ public function getActive()
+ {
+ return $this->_active;
+ }
+
+ /**
+ * @param boolean whether this transaction is active
+ */
+ protected function setActive($value)
+ {
+ $this->_active=TPropertyValue::ensureBoolean($value);
+ }
+}
+