Passed
Branch php-cs-fixer (b9836a)
by Fabio
15:02
created

getAssociationJoin()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
nc 3
nop 3
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
1
<?php
2
/**
3
 * TActiveRecordHasManyAssociation class file.
4
 *
5
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6
 * @link https://github.com/pradosoft/prado
7
 * @copyright Copyright &copy; 2005-2016 The PRADO Group
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 * @package Prado\Data\ActiveRecord\Relations
10
 */
11
12
namespace Prado\Data\ActiveRecord\Relations;
13
14
/**
15
 * Loads base active record relations class.
16
 */
17
use Prado\Data\ActiveRecord\TActiveRecord;
18
use Prado\Prado;
19
20
21
/**
22
 * Implements the M-N (many to many) relationship via association table.
23
 * Consider the <b>entity</b> relationship between Articles and Categories
24
 * via the association table <tt>Article_Category</tt>.
25
 * <code>
26
 * +---------+            +------------------+            +----------+
27
 * | Article | * -----> * | Article_Category | * <----- * | Category |
28
 * +---------+            +------------------+            +----------+
29
 * </code>
30
 * Where one article may have 0 or more categories and each category may have 0
31
 * or more articles. We may model Article-Category <b>object</b> relationship
32
 * as active record as follows.
33
 * <code>
34
 * class ArticleRecord
35
 * {
36
 *     const TABLE='Article';
37
 *     public $article_id;
38
 *
39
 *     public $Categories=array(); //foreign object collection.
40
 *
41
 *     public static $RELATIONS = array
42
 *     (
43
 *         'Categories' => array(self::MANY_TO_MANY, 'CategoryRecord', 'Article_Category')
44
 *     );
45
 *
46
 *     public static function finder($className=__CLASS__)
47
 *     {
48
 *         return parent::finder($className);
49
 *     }
50
 * }
51
 * class CategoryRecord
52
 * {
53
 *     const TABLE='Category';
54
 *     public $category_id;
55
 *
56
 *     public $Articles=array();
57
 *
58
 *     public static $RELATIONS = array
59
 *     (
60
 *         'Articles' => array(self::MANY_TO_MANY, 'ArticleRecord', 'Article_Category')
61
 *     );
62
 *
63
 *     public static function finder($className=__CLASS__)
64
 *     {
65
 *         return parent::finder($className);
66
 *     }
67
 * }
68
 * </code>
69
 *
70
 * The static <tt>$RELATIONS</tt> property of ArticleRecord defines that the
71
 * property <tt>$Categories</tt> has many <tt>CategoryRecord</tt>s. Similar, the
72
 * static <tt>$RELATIONS</tt> property of CategoryRecord defines many ArticleRecords.
73
 *
74
 * The articles with categories list may be fetched as follows.
75
 * <code>
76
 * $articles = TeamRecord::finder()->withCategories()->findAll();
77
 * </code>
78
 * The method <tt>with_xxx()</tt> (where <tt>xxx</tt> is the relationship property
79
 * name, in this case, <tt>Categories</tt>) fetchs the corresponding CategoryRecords using
80
 * a second query (not by using a join). The <tt>with_xxx()</tt> accepts the same
81
 * arguments as other finder methods of TActiveRecord.
82
 *
83
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
84
 * @package Prado\Data\ActiveRecord\Relations
85
 * @since 3.1
86
 */
87
class TActiveRecordHasManyAssociation extends TActiveRecordRelation
88
{
89
	private $_association;
90
	private $_sourceTable;
91
	private $_foreignTable;
92
	private $_association_columns = [];
93
94
	/**
95
	 * Get the foreign key index values from the results and make calls to the
96
	 * database to find the corresponding foreign objects using association table.
97
	 * @param array original results.
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\Relations\original was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
98
	 */
99
	protected function collectForeignObjects(&$results)
100
	{
101
		list($sourceKeys, $foreignKeys) = $this->getRelationForeignKeys();
102
		$properties = array_values($sourceKeys);
103
		$indexValues = $this->getIndexValues($properties, $results);
104
		$this->fetchForeignObjects($results, $foreignKeys, $indexValues, $sourceKeys);
105
	}
106
107
	/**
108
	 * @return array 2 arrays of source keys and foreign keys from the association table.
109
	 */
110
	public function getRelationForeignKeys()
111
	{
112
		$association = $this->getAssociationTable();
113
		$sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord(), true);
114
		$fkObject = $this->getContext()->getForeignRecordFinder();
115
		$foreignKeys = $this->findForeignKeys($association, $fkObject);
116
		return [$sourceKeys, $foreignKeys];
117
	}
118
119
	/**
120
	 * @return TDbTableInfo association table information.
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\Relations\TDbTableInfo was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
121
	 */
122
	protected function getAssociationTable()
123
	{
124
		if($this->_association === null)
125
		{
126
			$gateway = $this->getSourceRecord()->getRecordGateway();
127
			$conn = $this->getSourceRecord()->getDbConnection();
128
			//table name may include the fk column name separated with a dot.
129
			$table = explode('.', $this->getContext()->getAssociationTable());
130
			if(count($table) > 1)
131
			{
132
				$columns = preg_replace('/^\((.*)\)/', '\1', $table[1]);
133
				$this->_association_columns = preg_split('/\s*[, ]\*/', $columns);
134
			}
135
			$this->_association = $gateway->getTableInfo($conn, $table[0]);
136
		}
137
		return $this->_association;
138
	}
139
140
	/**
141
	 * @return TDbTableInfo source table information.
142
	 */
143
	protected function getSourceTable()
144
	{
145
		if($this->_sourceTable === null)
146
		{
147
			$gateway = $this->getSourceRecord()->getRecordGateway();
148
			$this->_sourceTable = $gateway->getRecordTableInfo($this->getSourceRecord());
149
		}
150
		return $this->_sourceTable;
151
	}
152
153
	/**
154
	 * @return TDbTableInfo foreign table information.
155
	 */
156
	protected function getForeignTable()
157
	{
158
		if($this->_foreignTable === null)
159
		{
160
			$gateway = $this->getSourceRecord()->getRecordGateway();
161
			$fkObject = $this->getContext()->getForeignRecordFinder();
162
			$this->_foreignTable = $gateway->getRecordTableInfo($fkObject);
163
		}
164
		return $this->_foreignTable;
165
	}
166
167
	/**
168
	 * @return TDataGatewayCommand
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\...ons\TDataGatewayCommand was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
169
	 */
170
	protected function getCommandBuilder()
171
	{
172
		return $this->getSourceRecord()->getRecordGateway()->getCommand($this->getSourceRecord());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getSourceR...his->getSourceRecord()) returns the type Prado\Data\DataGateway\TDataGatewayCommand which is incompatible with the documented return type Prado\Data\ActiveRecord\...ons\TDataGatewayCommand.
Loading history...
173
	}
174
175
	/**
176
	 * @return TDataGatewayCommand
177
	 */
178
	protected function getForeignCommandBuilder()
179
	{
180
		$obj = $this->getContext()->getForeignRecordFinder();
181
		return $this->getSourceRecord()->getRecordGateway()->getCommand($obj);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getSourceR...way()->getCommand($obj) returns the type Prado\Data\DataGateway\TDataGatewayCommand which is incompatible with the documented return type Prado\Data\ActiveRecord\...ons\TDataGatewayCommand.
Loading history...
182
	}
183
184
185
	/**
186
	 * Fetches the foreign objects using TActiveRecord::findAllByIndex()
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\Relations\foreign was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
187
	 * @param array field names
188
	 * @param array foreign key index values.
189
	 */
190
	protected function fetchForeignObjects(&$results, $foreignKeys, $indexValues, $sourceKeys)
191
	{
192
		$criteria = $this->getCriteria();
193
		$finder = $this->getContext()->getForeignRecordFinder();
194
		$type = get_class($finder);
195
		$command = $this->createCommand($criteria, $foreignKeys, $indexValues, $sourceKeys);
196
		$srcProps = array_keys($sourceKeys);
197
		$collections = [];
198
		foreach($this->getCommandBuilder()->onExecuteCommand($command, $command->query()) as $row)
199
		{
200
			$hash = $this->getObjectHash($row, $srcProps);
201
			foreach($srcProps as $column)
202
				unset($row[$column]);
203
			$obj = $this->createFkObject($type, $row, $foreignKeys);
204
			$collections[$hash][] = $obj;
205
		}
206
		$this->setResultCollection($results, $collections, array_values($sourceKeys));
207
	}
208
209
	/**
210
	 * @param string active record class name.
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\Relations\active was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
211
	 * @param array row data
212
	 * @param array foreign key column names
213
	 * @return TActiveRecord
214
	 */
215
	protected function createFkObject($type, $row, $foreignKeys)
216
	{
217
		$obj = TActiveRecord::createRecord($type, $row);
218
		if(count($this->_association_columns) > 0)
219
		{
220
			$i = 0;
221
			foreach($foreignKeys as $ref => $fk)
222
				$obj->setColumnValue($ref, $row[$this->_association_columns[$i++]]);
223
		}
224
		return $obj;
225
	}
226
227
	/**
228
	 * @param TSqlCriteria
229
	 * @param TTableInfo association table info
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\Relations\association was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
230
	 * @param array field names
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\Relations\field was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
231
	 * @param array field values
232
	 */
233
	public function createCommand($criteria, $foreignKeys, $indexValues, $sourceKeys)
234
	{
235
		$innerJoin = $this->getAssociationJoin($foreignKeys, $indexValues, $sourceKeys);
236
		$fkTable = $this->getForeignTable()->getTableFullName();
237
		$srcColumns = $this->getSourceColumns($sourceKeys);
238
		if(($where = $criteria->getCondition()) === null)
239
			$where = '1=1';
240
		$sql = "SELECT {$fkTable}.*, {$srcColumns} FROM {$fkTable} {$innerJoin} WHERE {$where}";
241
242
		$parameters = $criteria->getParameters()->toArray();
243
		$ordering = $criteria->getOrdersBy();
244
		$limit = $criteria->getLimit();
245
		$offset = $criteria->getOffset();
246
247
		$builder = $this->getForeignCommandBuilder()->getBuilder();
248
		$command = $builder->applyCriterias($sql, $parameters, $ordering, $limit, $offset);
249
		$this->getCommandBuilder()->onCreateCommand($command, $criteria);
250
		return $command;
251
	}
252
253
	/**
254
	 * @param array source table column names.
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\Relations\source was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
255
	 * @return string comma separated source column names.
256
	 */
257
	protected function getSourceColumns($sourceKeys)
258
	{
259
		$columns = [];
260
		$table = $this->getAssociationTable();
261
		$tableName = $table->getTableFullName();
262
		$columnNames = array_merge(array_keys($sourceKeys), $this->_association_columns);
263
		foreach($columnNames as $name)
264
			$columns[] = $tableName . '.' . $table->getColumn($name)->getColumnName();
265
		return implode(', ', $columns);
266
	}
267
268
	/**
269
	 * SQL inner join for M-N relationship via association table.
270
	 * @param array foreign table column key names.
271
	 * @param array source table index values.
272
	 * @param array source table column names.
273
	 * @return string inner join condition for M-N relationship via association table.
274
	 */
275
	protected function getAssociationJoin($foreignKeys, $indexValues, $sourceKeys)
276
	{
277
		$refInfo = $this->getAssociationTable();
278
		$fkInfo = $this->getForeignTable();
279
280
		$refTable = $refInfo->getTableFullName();
281
		$fkTable = $fkInfo->getTableFullName();
282
283
		$joins = [];
284
		$hasAssociationColumns = count($this->_association_columns) > 0;
285
		$i = 0;
286
		foreach($foreignKeys as $ref => $fk)
287
		{
288
			if($hasAssociationColumns)
289
				$refField = $refInfo->getColumn($this->_association_columns[$i++])->getColumnName();
290
			else
291
				$refField = $refInfo->getColumn($ref)->getColumnName();
292
			$fkField = $fkInfo->getColumn($fk)->getColumnName();
293
			$joins[] = "{$fkTable}.{$fkField} = {$refTable}.{$refField}";
294
		}
295
		$joinCondition = implode(' AND ', $joins);
296
		$index = $this->getCommandBuilder()->getIndexKeyCondition($refInfo, array_keys($sourceKeys), $indexValues);
297
		return "INNER JOIN {$refTable} ON ({$joinCondition}) AND {$index}";
298
	}
299
300
	/**
301
	 * Updates the associated foreign objects.
302
	 * @return boolean true if all update are success (including if no update was required), false otherwise .
303
	 */
304
	public function updateAssociatedRecords()
305
	{
306
		$obj = $this->getContext()->getSourceRecord();
307
		$fkObjects = &$obj->{$this->getContext()->getProperty()};
308
		$success = true;
309
		if(($total = count($fkObjects)) > 0)
310
		{
311
			$source = $this->getSourceRecord();
0 ignored issues
show
Unused Code introduced by
The assignment to $source is dead and can be removed.
Loading history...
312
			$builder = $this->getAssociationTableCommandBuilder();
313
			for($i = 0;$i < $total;$i++)
314
				$success = $fkObjects[$i]->save() && $success;
315
			return $this->updateAssociationTable($obj, $fkObjects, $builder) && $success;
316
		}
317
		return $success;
318
	}
319
320
	/**
321
	 * @return TDbCommandBuilder
0 ignored issues
show
Bug introduced by
The type Prado\Data\ActiveRecord\...tions\TDbCommandBuilder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
322
	 */
323
	protected function getAssociationTableCommandBuilder()
324
	{
325
		$conn = $this->getContext()->getSourceRecord()->getDbConnection();
326
		return $this->getAssociationTable()->createCommandBuilder($conn);
327
	}
328
329
	private function hasAssociationData($builder, $data)
330
	{
331
		$condition = [];
332
		$table = $this->getAssociationTable();
333
		foreach($data as $name => $value)
334
			$condition[] = $table->getColumn($name)->getColumnName() . ' = ?';
335
		$command = $builder->createCountCommand(implode(' AND ', $condition), array_values($data));
336
		$result = $this->getCommandBuilder()->onExecuteCommand($command, intval($command->queryScalar()));
337
		return intval($result) > 0;
338
	}
339
340
	private function addAssociationData($builder, $data)
341
	{
342
		$command = $builder->createInsertCommand($data);
343
		return $this->getCommandBuilder()->onExecuteCommand($command, $command->execute()) > 0;
344
	}
345
346
	private function updateAssociationTable($obj, $fkObjects, $builder)
347
	{
348
		$source = $this->getSourceRecordValues($obj);
349
		$foreignKeys = $this->findForeignKeys($this->getAssociationTable(), $fkObjects[0]);
350
		$success = true;
351
		foreach($fkObjects as $fkObject)
352
		{
353
			$data = array_merge($source, $this->getForeignObjectValues($foreignKeys, $fkObject));
354
			if(!$this->hasAssociationData($builder, $data))
355
				$success = $this->addAssociationData($builder, $data) && $success;
356
		}
357
		return $success;
358
	}
359
360
	private function getSourceRecordValues($obj)
361
	{
362
		$sourceKeys = $this->findForeignKeys($this->getAssociationTable(), $obj);
363
		$indexValues = $this->getIndexValues(array_values($sourceKeys), $obj);
364
		$data = [];
365
		$i = 0;
366
		foreach($sourceKeys as $name => $srcKey)
367
			$data[$name] = $indexValues[0][$i++];
368
		return $data;
369
	}
370
371
	private function getForeignObjectValues($foreignKeys, $fkObject)
372
	{
373
		$data = [];
374
		foreach($foreignKeys as $name => $fKey)
375
			$data[$name] = $fkObject->getColumnValue($fKey);
376
		return $data;
377
	}
378
}
379