]> git.sur5r.net Git - bacula/bacula/blob - gui/baculum/framework/Data/ActiveRecord/Relations/TActiveRecordHasManyAssociation.php
1b58f11261a89a6f26c9c7fb5a65076b720786d7
[bacula/bacula] / gui / baculum / framework / Data / ActiveRecord / Relations / TActiveRecordHasManyAssociation.php
1 <?php
2 /**
3  * TActiveRecordHasManyAssociation class file.
4  *
5  * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6  * @link http://www.pradosoft.com/
7  * @copyright Copyright &copy; 2005-2014 PradoSoft
8  * @license http://www.pradosoft.com/license/
9  * @version $Id$
10  * @package System.Data.ActiveRecord.Relations
11  */
12
13 /**
14  * Loads base active record relations class.
15  */
16 Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelation');
17
18 /**
19  * Implements the M-N (many to many) relationship via association table.
20  * Consider the <b>entity</b> relationship between Articles and Categories
21  * via the association table <tt>Article_Category</tt>.
22  * <code>
23  * +---------+            +------------------+            +----------+
24  * | Article | * -----> * | Article_Category | * <----- * | Category |
25  * +---------+            +------------------+            +----------+
26  * </code>
27  * Where one article may have 0 or more categories and each category may have 0
28  * or more articles. We may model Article-Category <b>object</b> relationship
29  * as active record as follows.
30  * <code>
31  * class ArticleRecord
32  * {
33  *     const TABLE='Article';
34  *     public $article_id;
35  *
36  *     public $Categories=array(); //foreign object collection.
37  *
38  *     public static $RELATIONS = array
39  *     (
40  *         'Categories' => array(self::MANY_TO_MANY, 'CategoryRecord', 'Article_Category')
41  *     );
42  *
43  *     public static function finder($className=__CLASS__)
44  *     {
45  *         return parent::finder($className);
46  *     }
47  * }
48  * class CategoryRecord
49  * {
50  *     const TABLE='Category';
51  *     public $category_id;
52  *
53  *     public $Articles=array();
54  *
55  *     public static $RELATIONS = array
56  *     (
57  *         'Articles' => array(self::MANY_TO_MANY, 'ArticleRecord', 'Article_Category')
58  *     );
59  *
60  *     public static function finder($className=__CLASS__)
61  *     {
62  *         return parent::finder($className);
63  *     }
64  * }
65  * </code>
66  *
67  * The static <tt>$RELATIONS</tt> property of ArticleRecord defines that the
68  * property <tt>$Categories</tt> has many <tt>CategoryRecord</tt>s. Similar, the
69  * static <tt>$RELATIONS</tt> property of CategoryRecord defines many ArticleRecords.
70  *
71  * The articles with categories list may be fetched as follows.
72  * <code>
73  * $articles = TeamRecord::finder()->withCategories()->findAll();
74  * </code>
75  * The method <tt>with_xxx()</tt> (where <tt>xxx</tt> is the relationship property
76  * name, in this case, <tt>Categories</tt>) fetchs the corresponding CategoryRecords using
77  * a second query (not by using a join). The <tt>with_xxx()</tt> accepts the same
78  * arguments as other finder methods of TActiveRecord.
79  *
80  * @author Wei Zhuo <weizho[at]gmail[dot]com>
81  * @version $Id$
82  * @package System.Data.ActiveRecord.Relations
83  * @since 3.1
84  */
85 class TActiveRecordHasManyAssociation extends TActiveRecordRelation
86 {
87         private $_association;
88         private $_sourceTable;
89         private $_foreignTable;
90         private $_association_columns=array();
91
92         /**
93         * Get the foreign key index values from the results and make calls to the
94         * database to find the corresponding foreign objects using association table.
95         * @param array original results.
96         */
97         protected function collectForeignObjects(&$results)
98         {
99                 list($sourceKeys, $foreignKeys) = $this->getRelationForeignKeys();
100                 $properties = array_values($sourceKeys);
101                 $indexValues = $this->getIndexValues($properties, $results);
102                 $this->fetchForeignObjects($results, $foreignKeys,$indexValues,$sourceKeys);
103         }
104
105         /**
106          * @return array 2 arrays of source keys and foreign keys from the association table.
107          */
108         public function getRelationForeignKeys()
109         {
110                 $association = $this->getAssociationTable();
111                 $sourceKeys = $this->findForeignKeys($association, $this->getSourceRecord(), true);
112                 $fkObject = $this->getContext()->getForeignRecordFinder();
113                 $foreignKeys = $this->findForeignKeys($association, $fkObject);
114                 return array($sourceKeys, $foreignKeys);
115         }
116
117         /**
118          * @return TDbTableInfo association table information.
119          */
120         protected function getAssociationTable()
121         {
122                 if($this->_association===null)
123                 {
124                         $gateway = $this->getSourceRecord()->getRecordGateway();
125                         $conn = $this->getSourceRecord()->getDbConnection();
126                         //table name may include the fk column name separated with a dot.
127                         $table = explode('.', $this->getContext()->getAssociationTable());
128                         if(count($table)>1)
129                         {
130                                 $columns = preg_replace('/^\((.*)\)/', '\1', $table[1]);
131                                 $this->_association_columns = preg_split('/\s*[, ]\*/',$columns);
132                         }
133                         $this->_association = $gateway->getTableInfo($conn, $table[0]);
134                 }
135                 return $this->_association;
136         }
137
138         /**
139          * @return TDbTableInfo source table information.
140          */
141         protected function getSourceTable()
142         {
143                 if($this->_sourceTable===null)
144                 {
145                         $gateway = $this->getSourceRecord()->getRecordGateway();
146                         $this->_sourceTable = $gateway->getRecordTableInfo($this->getSourceRecord());
147                 }
148                 return $this->_sourceTable;
149         }
150
151         /**
152          * @return TDbTableInfo foreign table information.
153          */
154         protected function getForeignTable()
155         {
156                 if($this->_foreignTable===null)
157                 {
158                         $gateway = $this->getSourceRecord()->getRecordGateway();
159                         $fkObject = $this->getContext()->getForeignRecordFinder();
160                         $this->_foreignTable = $gateway->getRecordTableInfo($fkObject);
161                 }
162                 return $this->_foreignTable;
163         }
164
165         /**
166          * @return TDataGatewayCommand
167          */
168         protected function getCommandBuilder()
169         {
170                 return $this->getSourceRecord()->getRecordGateway()->getCommand($this->getSourceRecord());
171         }
172
173         /**
174          * @return TDataGatewayCommand
175          */
176         protected function getForeignCommandBuilder()
177         {
178                 $obj = $this->getContext()->getForeignRecordFinder();
179                 return $this->getSourceRecord()->getRecordGateway()->getCommand($obj);
180         }
181
182
183         /**
184          * Fetches the foreign objects using TActiveRecord::findAllByIndex()
185          * @param array field names
186          * @param array foreign key index values.
187          */
188         protected function fetchForeignObjects(&$results,$foreignKeys,$indexValues,$sourceKeys)
189         {
190                 $criteria = $this->getCriteria();
191                 $finder = $this->getContext()->getForeignRecordFinder();
192                 $type = get_class($finder);
193                 $command = $this->createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys);
194                 $srcProps = array_keys($sourceKeys);
195                 $collections=array();
196                 foreach($this->getCommandBuilder()->onExecuteCommand($command, $command->query()) as $row)
197                 {
198                         $hash = $this->getObjectHash($row, $srcProps);
199                         foreach($srcProps as $column)
200                                 unset($row[$column]);
201                         $obj = $this->createFkObject($type,$row,$foreignKeys);
202                         $collections[$hash][] = $obj;
203                 }
204                 $this->setResultCollection($results, $collections, array_values($sourceKeys));
205         }
206
207         /**
208          * @param string active record class name.
209          * @param array row data
210          * @param array foreign key column names
211          * @return TActiveRecord
212          */
213         protected function createFkObject($type,$row,$foreignKeys)
214         {
215                 $obj = TActiveRecord::createRecord($type, $row);
216                 if(count($this->_association_columns) > 0)
217                 {
218                         $i=0;
219                         foreach($foreignKeys as $ref=>$fk)
220                                 $obj->setColumnValue($ref, $row[$this->_association_columns[$i++]]);
221                 }
222                 return $obj;
223         }
224
225         /**
226          * @param TSqlCriteria
227          * @param TTableInfo association table info
228          * @param array field names
229          * @param array field values
230          */
231         public function createCommand($criteria, $foreignKeys,$indexValues,$sourceKeys)
232         {
233                 $innerJoin = $this->getAssociationJoin($foreignKeys,$indexValues,$sourceKeys);
234                 $fkTable = $this->getForeignTable()->getTableFullName();
235                 $srcColumns = $this->getSourceColumns($sourceKeys);
236                 if(($where=$criteria->getCondition())===null)
237                         $where='1=1';
238                 $sql = "SELECT {$fkTable}.*, {$srcColumns} FROM {$fkTable} {$innerJoin} WHERE {$where}";
239
240                 $parameters = $criteria->getParameters()->toArray();
241                 $ordering = $criteria->getOrdersBy();
242                 $limit = $criteria->getLimit();
243                 $offset = $criteria->getOffset();
244
245                 $builder = $this->getForeignCommandBuilder()->getBuilder();
246                 $command = $builder->applyCriterias($sql,$parameters,$ordering,$limit,$offset);
247                 $this->getCommandBuilder()->onCreateCommand($command, $criteria);
248                 return $command;
249         }
250
251         /**
252          * @param array source table column names.
253          * @return string comma separated source column names.
254          */
255         protected function getSourceColumns($sourceKeys)
256         {
257                 $columns=array();
258                 $table = $this->getAssociationTable();
259                 $tableName = $table->getTableFullName();
260                 $columnNames = array_merge(array_keys($sourceKeys),$this->_association_columns);
261                 foreach($columnNames as $name)
262                         $columns[] = $tableName.'.'.$table->getColumn($name)->getColumnName();
263                 return implode(', ', $columns);
264         }
265
266         /**
267          * SQL inner join for M-N relationship via association table.
268          * @param array foreign table column key names.
269          * @param array source table index values.
270          * @param array source table column names.
271          * @return string inner join condition for M-N relationship via association table.
272          */
273         protected function getAssociationJoin($foreignKeys,$indexValues,$sourceKeys)
274         {
275                 $refInfo= $this->getAssociationTable();
276                 $fkInfo = $this->getForeignTable();
277
278                 $refTable = $refInfo->getTableFullName();
279                 $fkTable = $fkInfo->getTableFullName();
280
281                 $joins = array();
282                 $hasAssociationColumns = count($this->_association_columns) > 0;
283                 $i=0;
284                 foreach($foreignKeys as $ref=>$fk)
285                 {
286                         if($hasAssociationColumns)
287                                 $refField = $refInfo->getColumn($this->_association_columns[$i++])->getColumnName();
288                         else
289                                 $refField = $refInfo->getColumn($ref)->getColumnName();
290                         $fkField = $fkInfo->getColumn($fk)->getColumnName();
291                         $joins[] = "{$fkTable}.{$fkField} = {$refTable}.{$refField}";
292                 }
293                 $joinCondition = implode(' AND ', $joins);
294                 $index = $this->getCommandBuilder()->getIndexKeyCondition($refInfo,array_keys($sourceKeys), $indexValues);
295                 return "INNER JOIN {$refTable} ON ({$joinCondition}) AND {$index}";
296         }
297
298         /**
299          * Updates the associated foreign objects.
300          * @return boolean true if all update are success (including if no update was required), false otherwise .
301          */
302         public function updateAssociatedRecords()
303         {
304                 $obj = $this->getContext()->getSourceRecord();
305                 $fkObjects = &$obj->{$this->getContext()->getProperty()};
306                 $success=true;
307                 if(($total = count($fkObjects))> 0)
308                 {
309                         $source = $this->getSourceRecord();
310                         $builder = $this->getAssociationTableCommandBuilder();
311                         for($i=0;$i<$total;$i++)
312                                 $success = $fkObjects[$i]->save() && $success;
313                         return $this->updateAssociationTable($obj, $fkObjects, $builder) && $success;
314                 }
315                 return $success;
316         }
317
318         /**
319          * @return TDbCommandBuilder
320          */
321         protected function getAssociationTableCommandBuilder()
322         {
323                 $conn = $this->getContext()->getSourceRecord()->getDbConnection();
324                 return $this->getAssociationTable()->createCommandBuilder($conn);
325         }
326
327         private function hasAssociationData($builder,$data)
328         {
329                 $condition=array();
330                 $table = $this->getAssociationTable();
331                 foreach($data as $name=>$value)
332                         $condition[] = $table->getColumn($name)->getColumnName().' = ?';
333                 $command = $builder->createCountCommand(implode(' AND ', $condition),array_values($data));
334                 $result = $this->getCommandBuilder()->onExecuteCommand($command, intval($command->queryScalar()));
335                 return intval($result) > 0;
336         }
337
338         private function addAssociationData($builder,$data)
339         {
340                 $command = $builder->createInsertCommand($data);
341                 return $this->getCommandBuilder()->onExecuteCommand($command, $command->execute()) > 0;
342         }
343
344         private function updateAssociationTable($obj,$fkObjects, $builder)
345         {
346                 $source = $this->getSourceRecordValues($obj);
347                 $foreignKeys = $this->findForeignKeys($this->getAssociationTable(), $fkObjects[0]);
348                 $success=true;
349                 foreach($fkObjects as $fkObject)
350                 {
351                         $data = array_merge($source, $this->getForeignObjectValues($foreignKeys,$fkObject));
352                         if(!$this->hasAssociationData($builder,$data))
353                                 $success = $this->addAssociationData($builder,$data) && $success;
354                 }
355                 return $success;
356         }
357
358         private function getSourceRecordValues($obj)
359         {
360                 $sourceKeys = $this->findForeignKeys($this->getAssociationTable(), $obj);
361                 $indexValues = $this->getIndexValues(array_values($sourceKeys), $obj);
362                 $data = array();
363                 $i=0;
364                 foreach($sourceKeys as $name=>$srcKey)
365                         $data[$name] = $indexValues[0][$i++];
366                 return $data;
367         }
368
369         private function getForeignObjectValues($foreignKeys,$fkObject)
370         {
371                 $data=array();
372                 foreach($foreignKeys as $name=>$fKey)
373                         $data[$name] = $fkObject->getColumnValue($fKey);
374                 return $data;
375         }
376 }