summaryrefslogtreecommitdiff
path: root/framework/Data/ActiveRecord/Relations/TActiveRecordRelation.php
blob: 7fe2d46865615b596059d0cc620a4453f15aab25 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
<?php
/**
 * TActiveRecordRelation class file.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2014 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @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();
	}
}