From 4ceba82b9863f2c6323cbe00407e4bfbedbfc1cd Mon Sep 17 00:00:00 2001
From: wei <>
Date: Mon, 8 Oct 2007 03:24:07 +0000
Subject: Allow active records to have multiple foreign key references to the
 same table. Add TXCache.

---
 framework/Caching/TXCache.php                      | 126 +++++++++++++++++++++
 .../Relations/TActiveRecordBelongsTo.php           |  13 ++-
 .../Relations/TActiveRecordHasMany.php             |  17 ++-
 .../Relations/TActiveRecordHasManyAssociation.php  |  24 ++--
 .../ActiveRecord/Relations/TActiveRecordHasOne.php |  14 ++-
 .../Relations/TActiveRecordRelation.php            |  60 +++++++++-
 .../Relations/TActiveRecordRelationContext.php     |  91 ++++++++++-----
 framework/Data/ActiveRecord/TActiveRecord.php      |  75 +++++++++++-
 8 files changed, 361 insertions(+), 59 deletions(-)
 create mode 100644 framework/Caching/TXCache.php

(limited to 'framework')

diff --git a/framework/Caching/TXCache.php b/framework/Caching/TXCache.php
new file mode 100644
index 00000000..698020af
--- /dev/null
+++ b/framework/Caching/TXCache.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * TXCache class file
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link http://www.pradosoft.com/
+ * @copyright Copyright &copy; 2007 PradoSoft
+ * @license http://www.pradosoft.com/license/
+ * @version $Id: TXCache.php 1994 2007-06-11 16:02:28Z knut $
+ * @package System.Caching
+ */
+
+/**
+ * TXCache class
+ *
+ * TXCache implements a cache application module based on {@link http://xcache.lighttpd.net/ xcache}.
+ *
+ * By definition, cache does not ensure the existence of a value
+ * even if it never expires. Cache is not meant to be an persistent storage.
+ *
+ * To use this module, the xcache PHP extension must be loaded and configured in the php.ini.
+ *
+ * Some usage examples of TXCache are as follows,
+ * <code>
+ * $cache=new TXCache;  // TXCache may also be loaded as a Prado application module
+ * $cache->init(null);
+ * $cache->add('object',$object);
+ * $object2=$cache->get('object');
+ * </code>
+ *
+ * If loaded, TXCache will register itself with {@link TApplication} as the
+ * cache module. It can be accessed via {@link TApplication::getCache()}.
+ *
+ * TXCache may be configured in application configuration file as follows
+ * <code>
+ * <module id="cache" class="System.Caching.TXCache" />
+ * </code>
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @version $Id: TXCache.php 1994 2007-06-11 16:02:28Z knut $
+ * @package System.Caching
+ * @since 3.1.1
+ */
+class TXCache extends TCache
+{
+   /**
+    * Initializes this module.
+    * This method is required by the IModule interface.
+    * @param TXmlElement configuration for this module, can be null
+    * @throws TConfigurationException if xcache extension is not installed or not started, check your php.ini
+    */
+	public function init($config)
+	{
+		if(!function_exists('xcache_isset'))
+			throw new TConfigurationException('xcache_extension_required');
+
+		$enabled = intval(ini_get('xcache.cacher')) !== 0;
+		$var_size = intval(ini_get('xcache.var_size'));
+
+		if(!($enabled && $var_size > 0))
+			throw new TConfigurationException('xcache_extension_not_enabled');
+
+		parent::init($config);
+	}
+
+	/**
+	 * Retrieves a value from cache with a specified key.
+	 * This is the implementation of the method declared in the parent class.
+	 * @param string a unique key identifying the cached value
+	 * @return string the value stored in cache, false if the value is not in the cache or expired.
+	 */
+	protected function getValue($key)
+	{
+		return xcache_isset($key) ? xcache_get($key) : false;
+	}
+
+	/**
+	 * Stores a value identified by a key in cache.
+	 * This is the implementation of the method declared in the parent class.
+	 *
+	 * @param string the key identifying the value to be cached
+	 * @param string the value to be cached
+	 * @param integer the number of seconds in which the cached value will expire. 0 means never expire.
+	 * @return boolean true if the value is successfully stored into cache, false otherwise
+	 */
+	protected function setValue($key,$value,$expire)
+	{
+		return xcache_set($key,$value,$expire);
+	}
+
+	/**
+	 * Stores a value identified by a key into cache if the cache does not contain this key.
+	 * This is the implementation of the method declared in the parent class.
+	 *
+	 * @param string the key identifying the value to be cached
+	 * @param string the value to be cached
+	 * @param integer the number of seconds in which the cached value will expire. 0 means never expire.
+	 * @return boolean true if the value is successfully stored into cache, false otherwise
+	 */
+	protected function addValue($key,$value,$expire)
+	{
+		return !xcache_isset($key) ? $this->setValue($key,$value,$expire) : false;
+	}
+
+	/**
+	 * Deletes a value with the specified key from cache
+	 * This is the implementation of the method declared in the parent class.
+	 * @param string the key of the value to be deleted
+	 * @return boolean if no error happens during deletion
+	 */
+	protected function deleteValue($key)
+	{
+		return xcache_unset($key);
+	}
+
+	/**
+	 * Deletes all values from cache.
+	 * Be careful of performing this operation if the cache is shared by multiple applications.
+	 */
+	public function flush()
+	{
+		return xcache_clear_cache();
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php
index d7278d64..d4ff07e5 100644
--- a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php
@@ -77,8 +77,7 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation
 	 */
 	protected function collectForeignObjects(&$results)
 	{
-		$fkObject = $this->getContext()->getForeignRecordFinder();
-		$fkeys = $this->findForeignKeys($this->getSourceRecord(),$fkObject);
+		$fkeys = $this->getRelationForeignKeys();
 
 		$properties = array_keys($fkeys);
 		$fields = array_values($fkeys);
@@ -87,6 +86,16 @@ class TActiveRecordBelongsTo extends TActiveRecordRelation
 		$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.
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php
index cbba3ebd..f7426862 100644
--- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php
@@ -1,4 +1,4 @@
-<?php
+<?php
 /**
  * TActiveRecordHasMany class file.
  *
@@ -75,8 +75,7 @@ class TActiveRecordHasMany extends TActiveRecordRelation
 	 */
 	protected function collectForeignObjects(&$results)
 	{
-		$fkObject = $this->getContext()->getForeignRecordFinder();
-		$fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord());
+		$fkeys = $this->getRelationForeignKeys();
 
 		$properties = array_values($fkeys);
 		$fields = array_keys($fkeys);
@@ -86,6 +85,16 @@ class TActiveRecordHasMany extends TActiveRecordRelation
 		$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 .
@@ -113,5 +122,5 @@ class TActiveRecordHasMany extends TActiveRecordRelation
 		return $success;
 	}
 }
-
+
 ?>
\ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
index 0e176607..564d3d22 100644
--- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
@@ -37,7 +37,7 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
  *
  *     public static $RELATIONS = array
  *     (
- *         'Categories' => array(self::HAS_MANY, 'CategoryRecord', 'Article_Category')
+ *         'Categories' => array(self::MANY_TO_MANY, 'CategoryRecord', 'Article_Category')
  *     );
  *
  *     public static function finder($className=__CLASS__)
@@ -54,7 +54,7 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
  *
  *     public static $RELATIONS = array
  *     (
- *         'Articles' => array(self::HAS_MANY, 'ArticleRecord', 'Article_Category')
+ *         'Articles' => array(self::MANY_TO_MANY, 'ArticleRecord', 'Article_Category')
  *     );
  *
  *     public static function finder($className=__CLASS__)
@@ -96,17 +96,22 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation
 	*/
 	protected function collectForeignObjects(&$results)
 	{
-		$association = $this->getAssociationTable();
-		$sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord());
-
+		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);
-
-		$this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys);
+		return array($sourceKeys, $foreignKeys);
 	}
 
 	/**
@@ -182,7 +187,7 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation
 	 */
 	protected function fetchForeignObjects(&$results,$foreignKeys,$indexValues,$sourceKeys)
 	{
-		$criteria = $this->getContext()->getCriteria();
+		$criteria = $this->getCriteria();
 		$finder = $this->getContext()->getForeignRecordFinder();
 		$registry = $finder->getRecordManager()->getObjectStateRegistry();
 		$type = get_class($finder);
@@ -198,7 +203,6 @@ class TActiveRecordHasManyAssociation extends TActiveRecordRelation
 			$collections[$hash][] = $obj;
 			$registry->registerClean($obj);
 		}
-
 		$this->setResultCollection($results, $collections, array_values($sourceKeys));
 	}
 
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php
index b22428ae..e5a36659 100644
--- a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php
@@ -92,9 +92,7 @@ class TActiveRecordHasOne extends TActiveRecordRelation
 	 */
 	protected function collectForeignObjects(&$results)
 	{
-		$fkObject = $this->getContext()->getForeignRecordFinder();
-		$fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord());
-
+		$fkeys = $this->getRelationForeignKeys();
 		$properties = array_values($fkeys);
 		$fields = array_keys($fkeys);
 
@@ -102,6 +100,16 @@ class TActiveRecordHasOne extends TActiveRecordRelation
 		$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.
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php
index 5a3ea50e..8e9cc9b5 100644
--- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php
@@ -26,10 +26,12 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext');
 abstract class TActiveRecordRelation
 {
 	private $_context;
+	private $_criteria;
 
-	public function __construct(TActiveRecordRelationContext $context)
+	public function __construct(TActiveRecordRelationContext $context, $criteria)
 	{
 		$this->_context = $context;
+		$this->_criteria = $criteria;
 	}
 
 	/**
@@ -39,6 +41,14 @@ abstract class TActiveRecordRelation
 	{
 		return $this->_context;
 	}
+	
+	/**
+	 * @return TActiveRecordCriteria
+	 */
+	protected function getCriteria()
+	{
+		return $this->_criteria;
+	}
 
 	/**
 	 * @return TActiveRecord
@@ -75,6 +85,16 @@ abstract class TActiveRecordRelation
 			array_push($stack,$this); //call it later
 		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
@@ -84,7 +104,7 @@ abstract class TActiveRecordRelation
 	 * @param TActiveRecord $matchesRecord
 	 * @return array foreign keys with source column names as key and foreign column names as value.
 	 */
-	protected function findForeignKeys($from, $matchesRecord)
+	protected function findForeignKeys($from, $matchesRecord, $loose=false)
 	{
 		$gateway = $matchesRecord->getRecordGateway();
 		$matchingTableName = $gateway->getRecordTableInfo($matchesRecord)->getTableName();
@@ -93,13 +113,42 @@ abstract class TActiveRecordRelation
 			$tableInfo = $gateway->getRecordTableInfo($from);
 		foreach($tableInfo->getForeignKeys() as $fkeys)
 		{
-			if($fkeys['table']===$matchingTableName)
-				return $fkeys['keys'];
+			if(strtolower($fkeys['table'])===strtolower($matchingTableName))
+			{
+				if(!$loose && $this->getContext()->hasFkField())
+					return $this->getFkFields($fkeys['keys']);
+				else
+					return $fkeys['keys'];
+			}
 		}
 		$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
@@ -122,9 +171,8 @@ abstract class TActiveRecordRelation
 	 */
 	protected function findForeignObjects($fields, $indexValues)
 	{
-		$criteria = $this->getContext()->getCriteria();
 		$finder = $this->getContext()->getForeignRecordFinder();
-		return $finder->findAllByIndex($criteria, $fields, $indexValues);
+		return $finder->findAllByIndex($this->_criteria, $fields, $indexValues);
 	}
 
 	/**
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php
index 189d2c5e..d83aa63a 100644
--- a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php
@@ -32,13 +32,12 @@ class TActiveRecordRelationContext
 
 	private $_property;
 	private $_sourceRecord;
-	private $_criteria;
-	private $_relation;
+	private $_relation; //data from an entry of TActiveRecord::$RELATION
+	private $_fkeys;
 
-	public function __construct($source, $property=null, $criteria=null)
+	public function __construct($source, $property=null)
 	{
 		$this->_sourceRecord=$source;
-		$this->_criteria=$criteria;
 		if($property!==null)
 			list($this->_property, $this->_relation) = $this->getSourceRecordRelation($property);
 	}
@@ -48,7 +47,6 @@ class TActiveRecordRelationContext
 	 * property from the $RELATIONS static property in TActiveRecord.
 	 * @param string relation property name
 	 * @return array array($propertyName, $relation) relation definition.
-	 * @throws TActiveRecordException if property is not defined or missing.
 	 */
 	protected function getSourceRecordRelation($property)
 	{
@@ -58,8 +56,6 @@ class TActiveRecordRelationContext
 			if(strtolower($name)===$property)
 				return array($name, $relation);
 		}
-		throw new TActiveRecordException('ar_undefined_relation_prop',
-			$property, get_class($this->_sourceRecord), self::RELATIONS_CONST);
 	}
 
 	/**
@@ -71,6 +67,15 @@ class TActiveRecordRelationContext
 		return $class->getStaticPropertyValue(self::RELATIONS_CONST);
 	}
 
+	/**
+	 * @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();
@@ -85,14 +90,6 @@ class TActiveRecordRelationContext
 		return $this->_property;
 	}
 
-	/**
-	 * @return TActiveRecordCriteria sql query criteria for fetching the related record.
-	 */
-	public function getCriteria()
-	{
-		return $this->_criteria;
-	}
-
 	/**
 	 * @return TActiveRecord the active record instance that queried for its related records.
 	 */
@@ -109,6 +106,18 @@ class TActiveRecordRelationContext
 		return $this->_relation[1];
 	}
 
+	/**
+	 * @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
 	 */
@@ -117,6 +126,25 @@ class TActiveRecordRelationContext
 		return $this->_relation[0];
 	}
 
+	/**
+	 * @return string foreign key field names, comma delimited.
+	 * @since 3.1.2
+	 */
+	public function getFkField()
+	{
+		return $this->_relation[2];
+	}
+
+	/**
+	 * @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.
 	 */
@@ -130,7 +158,8 @@ class TActiveRecordRelationContext
 	 */
 	public function hasAssociationTable()
 	{
-		return isset($this->_relation[2]);
+		$isManyToMany = $this->getRelationType() === TActiveRecord::MANY_TO_MANY;
+		return $isManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]);
 	}
 
 	/**
@@ -145,29 +174,33 @@ class TActiveRecordRelationContext
 	 * 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()
+	public function getRelationHandler($criteria=null)
 	{
+		if(!$this->hasRecordRelation())
+		{
+			throw new TActiveRecordException('ar_undefined_relation_prop',
+				$property, get_class($this->_sourceRecord), self::RELATIONS_CONST);
+		}
+		if($criteria===null)
+			$criteria = new TActiveRecordCriteria;
 		switch($this->getRelationType())
 		{
 			case TActiveRecord::HAS_MANY:
-				if(!$this->hasAssociationTable())
-				{
-					Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasMany');
-					return new TActiveRecordHasMany($this);
-				}
-				else
-				{
-					Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasManyAssociation');
-					return new TActiveRecordHasManyAssociation($this);
-				}
+				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);
+				return new TActiveRecordHasOne($this, $criteria);
 			case TActiveRecord::BELONGS_TO:
 				Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordBelongsTo');
-				return new TActiveRecordBelongsTo($this);
+				return new TActiveRecordBelongsTo($this, $criteria);
 			default:
 				throw new TActiveRecordException('ar_invalid_relationship');
 		}
diff --git a/framework/Data/ActiveRecord/TActiveRecord.php b/framework/Data/ActiveRecord/TActiveRecord.php
index b53cdf45..446c87e4 100644
--- a/framework/Data/ActiveRecord/TActiveRecord.php
+++ b/framework/Data/ActiveRecord/TActiveRecord.php
@@ -94,9 +94,10 @@ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext');
  */
 abstract class TActiveRecord extends TComponent
 {
-	const HAS_MANY='HAS_MANY';
 	const BELONGS_TO='BELONGS_TO';
 	const HAS_ONE='HAS_ONE';
+	const HAS_MANY='HAS_MANY';
+	const MANY_TO_MANY='MANY_TO_MANY';
 
 	private static $_columnMapping=array();
 
@@ -110,6 +111,8 @@ abstract class TActiveRecord extends TComponent
 	 */
 	public static $COLUMN_MAPPING=array();
 
+	private static $_relations=array();
+
 	/**
 	 * This static variable defines the relationships.
 	 * The keys are public variable/property names defined in the AR class.
@@ -562,15 +565,77 @@ abstract class TActiveRecord extends TComponent
 	/**
 	 * Returns the active record relationship handler for $RELATION with key
 	 * value equal to the $property value.
-	 * @param string relationship property name.
+	 * @param string relationship/property name corresponding to keys in $RELATION array.
 	 * @param array method call arguments.
 	 * @return TActiveRecordRelation
 	 */
-	protected function getRelationHandler($property,$args)
+	protected function getRelationHandler($property,$args=array())
 	{
 		$criteria = $this->getCriteria(count($args)>0 ? $args[0] : null, array_slice($args,1));
-		$context = new TActiveRecordRelationContext($this, $property, $criteria);
-		return $context->getRelationHandler();
+		return $this->getRelationContext($property)->getRelationHandler($criteria);
+	}
+
+	/**
+	 * Gets a static copy of the relationship context for given property (a key
+	 * in $RELATION), 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 getRelationContext($property)
+	{
+		$prop = get_class($this).':'.$property;
+		if(!isset(self::$_relations[$prop]))
+		{
+			$context = new TActiveRecordRelationContext($this, $property);
+			//keep a null reference to all non-existing relations called?
+			self::$_relations[$prop] = $context->hasRecordRelation() ? $context : null;
+		}
+		return self::$_relations[$prop];
+	}
+
+	/**
+	 * 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->getRelationContext($property)) !== null)
+			return $context->getRelationHandler()->fetchResultsInto($this);
+		return false;
 	}
 
 	/**
-- 
cgit v1.2.3