summaryrefslogtreecommitdiff
path: root/framework/Data/ActiveRecord/Relations
diff options
context:
space:
mode:
Diffstat (limited to 'framework/Data/ActiveRecord/Relations')
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php43
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php96
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php149
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php42
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php172
-rw-r--r--framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php104
6 files changed, 606 insertions, 0 deletions
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php
new file mode 100644
index 00000000..3bb1a74b
--- /dev/null
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordBelongsTo.php
@@ -0,0 +1,43 @@
+<?php
+
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
+
+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)
+ {
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ $fkeys = $this->findForeignKeys($this->getSourceRecord(),$fkObject);
+
+ $properties = array_keys($fkeys);
+ $fields = array_values($fkeys);
+
+ $indexValues = $this->getIndexValues($properties, $results);
+ $fkObjects = $this->findForeignObjects($fields, $indexValues);
+ $this->populateResult($results,$properties,$fkObjects,$fields);
+ }
+
+ /**
+ * 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];
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php
new file mode 100644
index 00000000..27ffd194
--- /dev/null
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasMany.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * TActiveRecordHasMany class file.
+ *
+ * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
+ * @link http://www.pradosoft.com/
+ * @copyright Copyright &copy; 2005-2007 PradoSoft
+ * @license http://www.pradosoft.com/license/
+ * @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 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 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
+ *
+ * protected static $RELATIONS=array(
+ * 'players' => array(self::HAS_MANY, 'PlayerRecord'));
+ *
+ * public static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * 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 static function finder($className=__CLASS__)
+ * {
+ * return parent::finder($className);
+ * }
+ * }
+ * </code>
+ * The <tt>$RELATIONS</tt> static 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_player('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)
+ {
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ $fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord());
+
+ $properties = array_values($fkeys);
+ $fields = array_keys($fkeys);
+
+ $indexValues = $this->getIndexValues($properties, $results);
+ $fkObjects = $this->findForeignObjects($fields,$indexValues);
+ $this->populateResult($results,$properties,$fkObjects,$fields);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
new file mode 100644
index 00000000..a86fdffd
--- /dev/null
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * Loads base active record relations class.
+ */
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
+
+class TActiveRecordHasManyAssociation extends TActiveRecordRelation
+{
+ private $_association;
+ private $_sourceTable;
+ private $_foreignTable;
+
+ protected function collectForeignObjects(&$results)
+ {
+ $association = $this->getAssociationTable();
+ $sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord());
+
+ $properties = array_values($sourceKeys);
+
+ $indexValues = $this->getIndexValues($properties, $results);
+
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ $foreignKeys = $this->findForeignKeys($association, $fkObject);
+
+ $this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys);
+ }
+
+ protected function getAssociationTable()
+ {
+ if($this->_association===null)
+ {
+ $gateway = $this->getSourceRecord()->getRecordGateway();
+ $conn = $this->getSourceRecord()->getDbConnection();
+ $table = $this->getContext()->getAssociationTable();
+ $this->_association = $gateway->getTableInfo($conn, $table);
+ }
+ return $this->_association;
+ }
+
+ protected function getSourceTable()
+ {
+ if($this->_sourceTable===null)
+ {
+ $gateway = $this->getSourceRecord()->getRecordGateway();
+ $this->_sourceTable = $gateway->getRecordTableInfo($this->getSourceRecord());
+ }
+ return $this->_sourceTable;
+ }
+
+ protected function getForeignTable()
+ {
+ if($this->_foreignTable===null)
+ {
+ $gateway = $this->getSourceRecord()->getRecordGateway();
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ $this->_foreignTable = $gateway->getRecordTableInfo($fkObject);
+ }
+ return $this->_foreignTable;
+ }
+
+ protected function getCommandBuilder()
+ {
+ return $this->getSourceRecord()->getRecordGateway()->getCommand($this->getSourceRecord());
+ }
+
+ /**
+ * 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->getContext()->getCriteria();
+ $finder = $this->getContext()->getForeignRecordFinder();
+ $command = $this->createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys);
+ $srcProps = array_keys($sourceKeys);
+ $type = get_class($finder);
+ $collections=array();
+ foreach($command->query() as $row)
+ {
+ $hash = $this->getObjectHash($row, $srcProps);
+ foreach($srcProps as $column)
+ unset($row[$column]);
+ $collections[$hash][] = $finder->populateObject($type,$row);
+ }
+
+ $this->setResultCollection($results, $collections, array_values($sourceKeys));
+ }
+
+ /**
+ * @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->getCommandBuilder()->getBuilder();
+ $command = $builder->applyCriterias($sql,$parameters,$ordering,$limit,$offset);
+ $this->getCommandBuilder()->onCreateCommand($command, $criteria);
+ return $command;
+ }
+
+ protected function getSourceColumns($sourceKeys)
+ {
+ $columns=array();
+ $table = $this->getAssociationTable();
+ $tableName = $table->getTableFullName();
+ foreach($sourceKeys as $name=>$fkName)
+ $columns[] = $tableName.'.'.$table->getColumn($name)->getColumnName();
+ return implode(', ', $columns);
+ }
+
+ protected function getAssociationJoin($foreignKeys,$indexValues,$sourceKeys)
+ {
+ $refInfo= $this->getAssociationTable();
+ $fkInfo = $this->getForeignTable();
+
+ $refTable = $refInfo->getTableFullName();
+ $fkTable = $fkInfo->getTableFullName();
+
+ $joins = array();
+ foreach($foreignKeys as $ref=>$fk)
+ {
+ $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}";
+ }
+}
+?> \ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php
new file mode 100644
index 00000000..048f776b
--- /dev/null
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordHasOne.php
@@ -0,0 +1,42 @@
+<?php
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
+
+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)
+ {
+ $fkObject = $this->getContext()->getForeignRecordFinder();
+ $fkeys = $this->findForeignKeys($fkObject, $this->getSourceRecord());
+
+ $properties = array_values($fkeys);
+ $fields = array_keys($fkeys);
+
+ $indexValues = $this->getIndexValues($properties, $results);
+ $fkObjects = $this->findForeignObjects($fields,$indexValues);
+ $this->populateResult($results,$properties,$fkObjects,$fields);
+ }
+
+ /**
+ * 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];
+ }
+ }
+}
+
+?> \ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php
new file mode 100644
index 00000000..f1e8fa60
--- /dev/null
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php
@@ -0,0 +1,172 @@
+<?php
+Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext');
+
+abstract class TActiveRecordRelation
+{
+ private $_context;
+
+ public function __construct(TActiveRecordRelationContext $context)
+ {
+ $this->_context = $context;
+ }
+
+ /**
+ * @return TActiveRecordRelationContext
+ */
+ protected function getContext()
+ {
+ return $this->_context;
+ }
+
+ /**
+ * @return TActiveRecord
+ */
+ protected function getSourceRecord()
+ {
+ return $this->getContext()->getSourceRecord();
+ }
+
+ /**
+ * Dispatch the method calls to the source record finder object. When
+ * the results are returned as array or is an instance of TActiveRecord we
+ * will fetch the corresponding foreign objects with an sql query and populate
+ * the results obtained earlier.
+ *
+ * Allows chaining multiple relation handlers.
+ *
+ * @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);
+ if(is_array($results) || $results instanceof TActiveRecord)
+ {
+ $this->collectForeignObjects($results);
+ while($obj = array_pop($stack))
+ $obj->collectForeignObjects($results);
+ }
+ else if($results instanceof TActiveRecordRelation)
+ array_push($stack,$this); //call it later
+ return $results;
+ }
+
+ /**
+ * 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)
+ {
+ $gateway = $matchesRecord->getRecordGateway();
+ $matchingTableName = $gateway->getRecordTableInfo($matchesRecord)->getTableName();
+ $tableInfo=$from;
+ if($from instanceof TActiveRecord)
+ $tableInfo = $gateway->getRecordTableInfo($from);
+ foreach($tableInfo->getForeignKeys() as $fkeys)
+ {
+ if($fkeys['table']===$matchingTableName)
+ return $fkeys['keys'];
+ }
+ throw new TActiveRecordException('no fk defined for '.$tableInfo->getTableFullName());
+ }
+
+ /**
+ * @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) ? $obj->{$property} : $obj[$property];
+ return sprintf('%x',crc32(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)
+ {
+ $criteria = $this->getContext()->getCriteria();
+ $finder = $this->getContext()->getForeignRecordFinder();
+ return $finder->findAllByIndex($criteria, $fields, $indexValues);
+ }
+
+ /**
+ * Obtain the foreign key index values from the results.
+ * @param array property names
+ * @param array|TActiveRecord TActiveRecord results
+ * @return array foreign key index values.
+ */
+ protected function getIndexValues($keys, $results)
+ {
+ if(!is_array($results))
+ $results = array($results);
+ foreach($results as $result)
+ {
+ $value = array();
+ foreach($keys as $name)
+ $value[] = $result->{$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)
+ {
+ $hash = $this->getObjectHash($fkObject, $fields);
+ $collections[$hash][]=$fkObject;
+ }
+
+ $this->setResultCollection($results, $collections, $properties);
+ }
+
+ protected function setResultCollection(&$results, &$collections, $properties)
+ {
+ if(is_array($results))
+ {
+ 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();
+ }
+}
+
+?> \ No newline at end of file
diff --git a/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php
new file mode 100644
index 00000000..03dc4cd5
--- /dev/null
+++ b/framework/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php
@@ -0,0 +1,104 @@
+<?php
+
+class TActiveRecordRelationContext
+{
+ const RELATIONS_CONST = 'RELATIONS';
+
+ private $_property;
+ private $_sourceRecord;
+ private $_criteria;
+ private $_relation;
+
+ public function __construct($source, $property, $criteria)
+ {
+ $this->_sourceRecord=$source;
+ $this->_property=$property;
+ $this->_criteria=$criteria;
+ $this->_relation = $this->getSourceRecordRelation($property);
+ }
+ /**
+ * @param string relation property name
+ * @return array relation definition.
+ */
+ protected function getSourceRecordRelation($property)
+ {
+ $class = new ReflectionClass($this->_sourceRecord);
+ $statics = $class->getStaticProperties();
+ if(!isset($statics[self::RELATIONS_CONST]))
+ throw new TActiveRecordException('ar_relations_undefined',
+ get_class($this->_sourceRecord), self::RELATIONS_CONST);
+ if(isset($statics[self::RELATIONS_CONST][$property]))
+ return $statics[self::RELATIONS_CONST][$property];
+ else
+ throw new TActiveRecordException('ar_undefined_relation_prop',
+ $property, get_class($this->_sourceRecord), self::RELATIONS_CONST);
+ }
+
+ public function getProperty()
+ {
+ return $this->_property;
+ }
+
+ public function getCriteria()
+ {
+ return $this->_criteria;
+ }
+
+ public function getSourceRecord()
+ {
+ return $this->_sourceRecord;
+ }
+
+ public function getForeignRecordClass()
+ {
+ return $this->_relation[1];
+ }
+
+ public function getRelationType()
+ {
+ return $this->_relation[0];
+ }
+
+ public function getAssociationTable()
+ {
+ return $this->_relation[2];
+ }
+
+ public function hasAssociationTable()
+ {
+ return isset($this->_relation[2]);
+ }
+
+ public function getForeignRecordFinder()
+ {
+ return TActiveRecord::finder($this->getForeignRecordClass());
+ }
+
+ public function getRelationHandler()
+ {
+ 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);
+ }
+ case TActiveRecord::HAS_ONE:
+ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasOne');
+ return new TActiveRecordHasOne($this);
+ case TActiveRecord::BELONGS_TO:
+ Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordBelongsTo');
+ return new TActiveRecordBelongsTo($this);
+ default:
+ throw new TException('Not done yet');
+ }
+ }
+}
+
+?> \ No newline at end of file