<?php

Prado::using('System.DataAccess.SQLMap.DataMapper.*');
Prado::using('System.DataAccess.SQLMap.Configuration.*');
Prado::using('System.DataAccess.SQLMap.Statements.*');
Prado::using('System.Collections.*');
Prado::using('System.DataAccess.SQLMap.DataMapper.TTypeHandlerFactory');
Prado::using('System.DataAccess.SQLMap.DataMapper.TSqlMapCache');
Prado::using('System.DataAccess.SQLMap.DataMapper.TDataMapperException');
Prado::using('System.DataAccess.TAdodb');

/**
 * DataMapper client, a facade 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
 *
 * Do not create this class explicitly, use TDomSqlMapBuilder to obtain
 * an instance by parsing through the xml configurations. Example:
 * <code>
 * $builder = new TDomSqlMapBuilder(); 
 * $mapper = $builder->configure($configFile);
 * </code>
 *
 * Otherwise use convient classes TMapper or TSqlMap to obtain singleton
 * instances.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @version $Revision: $  $Date: $
 * @package System.DataAccess.SQLMap
 * @since 3.0
 */
class TSqlMapper extends TComponent
{
	private $_connection;
	private $_mappedStatements;
	private $_provider;
	private $_resultMaps;
	private $_parameterMaps;
	private $_typeHandlerFactory;
	private $_cacheModelsEnabled = true;
	private $_cacheMaps;

	/**
	 * Create a new SqlMap.
	 * @param TTypeHandlerFactory
	 */
	public function __construct($typeHandlerFactory=null)
	{
		$this->_mappedStatements = new TMap;
		$this->_resultMaps = new TMap;
		$this->_parameterMaps = new TMap;
		$this->_typeHandlerFactory = $typeHandlerFactory;
		$this->_cacheMaps = new TMap;
	}

	/**
	 * Cleanup work before serializing.
	 * This is a PHP defined magic method.
	 * @return array the names of instance-variables to serialize.
	 */
	public function __sleep()
	{
		if(!is_null($this->_connection) && !$this->_connection->getIsClosed())
			$this->closeConnection();
		$this->_connection = null;
		return array_keys(get_object_vars($this));
	}
	
	/**
	 * This method will be automatically called when unserialization happens.
	 * This is a PHP defined magic method.
	 */
	public function __wake()
	{

	}
	
	/**
	 * Set the falg to tell us if cache models were enabled or not.
	 * This should only be called during configuration parsing.
	 * It does not disable the cache after the configuration phase.
	 * @param boolean enables cache.
	 */
	public function setCacheModelsEnabled($value)
	{
		$this->_cacheModelsEnabled = $value;
	}

	/**
	 * @return boolean true if cache models were enabled  when this SqlMap was
	 * built.
	 */
	public function getIsCacheModelsEnabled()
	{
		return $this->_cacheModelsEnabled;
	}

	/**
	 * @return TTypeHandlerFactory The TypeHandlerFactory
	 */
	public function getTypeHandlerFactory()
	{
		return $this->_typeHandlerFactory;
	}
	
	/**
	 * @return TMap mapped statements collection.
	 */
	public function getStatements()
	{
		return $this->_mappedStatements;
	}

	/**
	 * @return TMap result maps collection.
	 */
	public function getResultMaps()
	{
		return $this->_resultMaps;
	}

	/**
	 * Adds a named cache.
	 * @param TSqlMapCacheModel the cache to add.
	 * @throws TSqlMapConfigurationException
	 */
	public function addCache(TSqlMapCacheModel $cacheModel)
	{
		if($this->_cacheMaps->contains($cacheModel->getID()))
			throw new TSqlMapConfigurationException(
				'sqlmap_cache_model_already_exists', $cacheModel->getID());
		else
			$this->_cacheMaps->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 getCache($name)
	{
		if(!$this->_cacheMaps->contains($name))
			throw new TSqlMapConfigurationException(
				'sqlmap_unable_to_find_cache_model', $name);
		return $this->_cacheMaps[$name];
	}

	/**
	 * Flushes all cached objects that belong to this SqlMap
	 */
	public function flushCaches()
	{
		foreach($this->_cacheMaps as $cache)
			$cache->flush();
	}

	/**
	 * @return TMap parameter maps collection.
	 */
	public function getParameterMaps()
	{
		return $this->_parameterMaps;
	}

	/**
	 * 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);
	}
	
	/**
	 * 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);
	}

	/**
	 * @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);
	}

	/**
	 * @param TDatabaseProvider changes the database provider.
	 */
	public function setDataProvider($provider)
	{
		$this->_provider = $provider;
	}

	/**
	 * @return TDatabaseProvider database provider.
	 */
	public function getDataProvider()
	{
		return $this->_provider;
	}

	/**
	 * Get the current connection, opens the connection if necessary.
	 * @return TDbConnection database connection.
	 */
	protected function getConnection()
	{
		if(is_null($this->_connection))
			$this->_connection = $this->getDataProvider()->getConnection();
		$this->_connection->open();
		return $this->_connection;
	}

	/**
	 * Open a connection, on the specified connection string if provided.
	 * @param string The connection DSN string
	 * @return TDbConnection database connection.
	 */
	public function openConnection($connectionString=null)
	{
		if(!is_null($connectionString))
		{
			if(!is_null($this->_connection))
				throw new TSqlMapConnectionException(
					'sqlmap_connection_already_exists');
			$this->getDataProvider()->setConnectionString($connectionString);
		}
		return $this->getConnection();
	}

	/**
	 * Close the current database connection.
	 */
	public function closeConnection()
	{
		if(is_null($this->_connection))
			throw new TSqlMapConnectionException(
				'sqlmap_unable_to_close_null_connection');
		$this->_connection->close();
	}

	/**
	 * 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->getMappedStatement($statementName);
		$connection = $this->getConnection();
		return $statement->executeQueryForObject($connection, 
									$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->getMappedStatement($statementName);
		$connection = $this->getConnection();
		return $statement->executeQueryForList($connection, 
								$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->getMappedStatement($statementName);
		$connection = $this->getConnection();
		return $statement->executeQueryForList($connection, 
								$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.
	 * @return TPagedList A PaginatedList of beans containing the rows.
	 */
	public function queryForPagedList($statementName, $parameter=null, $pageSize=10)
	{
		$statement = $this->getMappedStatement($statementName);
		return new TSqlMapPagedList($statement, $parameter, $pageSize);
	}

	/**
	 * 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.
	 * @return TPagedList A PaginatedList of beans containing the rows.
	 */
	public function queryForPagedListWithRowDelegate($statementName, 
								$delegate, $parameter=null, $pageSize=10)
	{
		$statement = $this->getMappedStatement($statementName);
		return new TSqlMapPagedList($statement, $parameter, $pageSize, $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.
	 * @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) 
	{
		$statement = $this->getMappedStatement($statementName);
		$connection = $this->getConnection();
		return $statement->executeQueryForMap($connection, 
								$parameter, $keyProperty, $valueProperty);
	}

	/**
	 * 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) 
	{
		$statement = $this->getMappedStatement($statementName);
		$connection = $this->getConnection();
		return $statement->executeQueryForMap($connection, 
								$parameter, $keyProperty, $valueProperty, $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->getMappedStatement($statementName);
		$connection = $this->getConnection();
		$generatedKey = $statement->executeInsert($connection, $parameter);
		return $generatedKey;
	}

	/**
	 * 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->getMappedStatement($statementName);
		$connection = $this->getConnection();
		return $statement->executeUpdate($connection, $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);
	}


	/**
	 * Begins a database transaction on the currect session.
	 * Some databases will always return false if transaction support is not
	 * available
	 * @return boolean true if successful, false otherwise.
	 */
	public function beginTransaction()
	{
		return $this->getConnection()->beginTransaction();
	}

	/**
	 * End a transaction successfully. If the database does not support 
	 * transactions, will return true also as data is always committed.
	 * @return boolean true if successful, false otherwise.
	 */
	public function commitTransaction()
	{
		return $this->getConnection()->commit();
	}

	/**
	 * End a transaction, rollback all changes. If the database does not 
	 * support transactions, will return false as data is never rollbacked.
	 * @return boolean true if successful, false otherwise.
	 */
	public function rollbackTransaction()
	{
		return $this->getConnection()->rollback();
	}
}

?>