1 <?php
2 /**
3 * @package FrameworkOnFramework
4 * @subpackage table
5 * @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE.txt
7 */
8
9 // Protect from unauthorized access
10 defined('FOF_INCLUDED') or die;
11
12 class FOFTableRelations
13 {
14 /**
15 * Holds all known relation definitions
16 *
17 * @var array
18 */
19 protected $relations = array(
20 'child' => array(),
21 'parent' => array(),
22 'children' => array(),
23 'multiple' => array(),
24 );
25
26 /**
27 * Holds the default relations' keys
28 *
29 * @var array
30 */
31 protected $defaultRelation = array(
32 'child' => null,
33 'parent' => null,
34 'children' => null,
35 'multiple' => null,
36 );
37
38 /**
39 * The table these relations are attached to
40 *
41 * @var FOFTable
42 */
43 protected $table = null;
44
45 /**
46 * The name of the component used by our attached table
47 *
48 * @var string
49 */
50 protected $componentName = 'joomla';
51
52 /**
53 * The type (table name without prefix and component name) of our attached table
54 *
55 * @var string
56 */
57 protected $tableType = '';
58
59
60 /**
61 * Create a relations object based on the provided FOFTable instance
62 *
63 * @param FOFTable $table The table instance used to initialise the relations
64 */
65 public function __construct(FOFTable $table)
66 {
67 // Store the table
68 $this->table = $table;
69
70 // Get the table's type from its name
71 $tableName = $table->getTableName();
72 $tableName = str_replace('#__', '', $tableName);
73 $type = explode("_", $tableName);
74
75 if (count($type) == 1)
76 {
77 $this->tableType = array_pop($type);
78 }
79 else
80 {
81 $this->componentName = array_shift($type);
82 $this->tableType = array_pop($type);
83 }
84
85 $this->tableType = FOFInflector::singularize($this->tableType);
86
87 $tableKey = $table->getKeyName();
88
89 unset($type);
90
91 // Scan all table keys and look for foo_bar_id fields. These fields are used to populate parent relations.
92 foreach ($table->getKnownFields() as $field)
93 {
94 // Skip the table key name
95 if ($field == $tableKey)
96 {
97 continue;
98 }
99
100 if (substr($field, -3) != '_id')
101 {
102 continue;
103 }
104
105 $parts = explode('_', $field);
106
107 // If the component type of the field is not set assume 'joomla'
108 if (count($parts) == 2)
109 {
110 array_unshift($parts, 'joomla');
111 }
112
113 // Sanity check
114 if (count($parts) != 3)
115 {
116 continue;
117 }
118
119 // Make sure we skip any references back to ourselves (should be redundant, due to key field check above)
120 if ($parts[1] == $this->tableType)
121 {
122 continue;
123 }
124
125 // Default item name: the name of the table, singular
126 $itemName = FOFInflector::singularize($parts[1]);
127
128 // Prefix the item name with the component name if we refer to a different component
129 if ($parts[0] != $this->componentName)
130 {
131 $itemName = $parts[0] . '_' . $itemName;
132 }
133
134 // Figure out the table class
135 $tableClass = ucfirst($parts[0]) . 'Table' . ucfirst($parts[1]);
136
137 $default = empty($this->relations['parent']);
138
139 $this->addParentRelation($itemName, $tableClass, $field, $field, $default);
140 }
141
142 // Get the relations from the configuration provider
143 $key = $table->getConfigProviderKey() . '.relations';
144 $configRelations = $table->getConfigProvider()->get($key, array());
145
146 if (!empty($configRelations))
147 {
148 foreach ($configRelations as $relation)
149 {
150 if (empty($relation['type']))
151 {
152 continue;
153 }
154
155 if (isset($relation['pivotTable']))
156 {
157 $this->addMultipleRelation($relation['itemName'], $relation['tableClass'],
158 $relation['localKey'], $relation['ourPivotKey'], $relation['theirPivotKey'],
159 $relation['remoteKey'], $relation['pivotTable'], $relation['default']);
160 }
161 else
162 {
163 $method = 'add' . ucfirst($relation['type']). 'Relation';
164
165 if (!method_exists($this, $method))
166 {
167 continue;
168 }
169
170 $this->$method($relation['itemName'], $relation['tableClass'],
171 $relation['localKey'], $relation['remoteKey'], $relation['default']);
172 }
173 }
174 }
175
176 }
177
178 /**
179 * Add a 1:1 forward (child) relation. This adds relations for the getChild() method.
180 *
181 * In other words: does a table HAVE ONE child
182 *
183 * Parent and child relations works the same way. We have them separated as it makes more sense for us humans to
184 * read code like $item->getParent() and $item->getChild() than $item->getRelatedObject('someRandomKeyName')
185 *
186 * @param string $itemName is how it will be known locally to the getRelatedItem method (singular)
187 * @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
188 * @param string $localKey is the column containing our side of the FK relation, default: our primary key
189 * @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
190 * @param boolean $default add as the default child relation?
191 *
192 * @return void
193 */
194 public function addChildRelation($itemName, $tableClass = null, $localKey = null, $remoteKey = null, $default = true)
195 {
196 $itemName = $this->normaliseItemName($itemName, false);
197
198 if (empty($localKey))
199 {
200 $localKey = $this->table->getKeyName();
201 }
202
203 $this->addBespokeSimpleRelation('child', $itemName, $tableClass, $localKey, $remoteKey, $default);
204 }
205
206 /**
207 * Defining an inverse 1:1 (parent) relation. You must specify at least the $tableClass or the $localKey.
208 * This adds relations for the getParent() method.
209 *
210 * In other words: does a table BELONG TO ONE parent
211 *
212 * Parent and child relations works the same way. We have them separated as it makes more sense for us humans to
213 * read code like $item->getParent() and $item->getChild() than $item->getRelatedObject('someRandomKeyName')
214 *
215 * @param string $itemName is how it will be known locally to the getRelatedItem method (singular)
216 * @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
217 * @param string $localKey is the column containing our side of the FK relation, default: componentname_itemname_id
218 * @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
219 * @param boolean $default Is this the default parent relationship?
220 *
221 * @return void
222 */
223 public function addParentRelation($itemName, $tableClass = null, $localKey = null, $remoteKey = null, $default = true)
224 {
225 $itemName = $this->normaliseItemName($itemName, false);
226
227 $this->addBespokeSimpleRelation('parent', $itemName, $tableClass, $localKey, $remoteKey, $default);
228 }
229
230 /**
231 * Defining a forward 1:∞ (children) relation. This adds relations to the getChildren() method.
232 *
233 * In other words: does a table HAVE MANY children?
234 *
235 * The children relation works very much the same as the parent and child relation. The difference is that the
236 * parent and child relations return a single table object, whereas the children relation returns an iterator to
237 * many objects.
238 *
239 * @param string $itemName is how it will be known locally to the getRelatedItems method (plural)
240 * @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
241 * @param string $localKey is the column containing our side of the FK relation, default: our primary key
242 * @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
243 * @param boolean $default is this the default children relationship?
244 *
245 * @return void
246 */
247 public function addChildrenRelation($itemName, $tableClass = null, $localKey = null, $remoteKey = null, $default = true)
248 {
249 $itemName = $this->normaliseItemName($itemName, true);
250
251 if (empty($localKey))
252 {
253 $localKey = $this->table->getKeyName();
254 }
255
256 $this->addBespokeSimpleRelation('children', $itemName, $tableClass, $localKey, $remoteKey, $default);
257 }
258
259 /**
260 * Defining a ∞:∞ (multiple) relation. This adds relations to the getMultiple() method.
261 *
262 * In other words: is a table RELATED TO MANY other records?
263 *
264 * @param string $itemName is how it will be known locally to the getRelatedItems method (plural)
265 * @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
266 * @param string $localKey is the column containing our side of the FK relation, default: our primary key field name
267 * @param string $ourPivotKey is the column containing our side of the FK relation in the pivot table, default: $localKey
268 * @param string $theirPivotKey is the column containing the other table's side of the FK relation in the pivot table, default $remoteKey
269 * @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
270 * @param string $glueTable is the name of the glue (pivot) table, default: #__componentname_thisclassname_itemname with plural items (e.g. #__foobar_users_roles)
271 * @param boolean $default is this the default multiple relation?
272 */
273 public function addMultipleRelation($itemName, $tableClass = null, $localKey = null, $ourPivotKey = null, $theirPivotKey = null, $remoteKey = null, $glueTable = null, $default = true)
274 {
275 $itemName = $this->normaliseItemName($itemName, true);
276
277 if (empty($localKey))
278 {
279 $localKey = $this->table->getKeyName();
280 }
281
282 $this->addBespokePivotRelation('multiple', $itemName, $tableClass, $localKey, $remoteKey, $ourPivotKey, $theirPivotKey, $glueTable, $default);
283 }
284
285 /**
286 * Removes a previously defined relation by name. You can optionally specify the relation type.
287 *
288 * @param string $itemName The name of the relation to remove
289 * @param string $type [optional] The relation type (child, parent, children, ...)
290 *
291 * @return void
292 */
293 public function removeRelation($itemName, $type = null)
294 {
295 $types = array_keys($this->relations);
296
297 if (in_array($type, $types))
298 {
299 $types = array($type);
300 }
301
302 foreach ($types as $type)
303 {
304 foreach ($this->relations[$type] as $key => $relations)
305 {
306 if ($itemName == $key)
307 {
308 unset ($this->relations[$type][$itemName]);
309
310 // If it's the default one, remove it from the default array, too
311 if($this->defaultRelation[$type] == $itemName)
312 {
313 $this->defaultRelation[$type] = null;
314 }
315
316 return;
317 }
318 }
319 }
320 }
321
322 /**
323 * Removes all existing relations
324 *
325 * @param string $type The type or relations to remove, omit to remove all relation types
326 *
327 * @return void
328 */
329 public function clearRelations($type = null)
330 {
331 $types = array_keys($this->relations);
332
333 if (in_array($type, $types))
334 {
335 $types = array($type);
336 }
337
338 foreach ($types as $type)
339 {
340 $this->relations[$type] = array();
341
342 // Remove the relation from the default stack, too
343 $this->defaultRelation[$type] = null;
344 }
345 }
346
347 /**
348 * Does the named relation exist? You can optionally specify the type.
349 *
350 * @param string $itemName The name of the relation to check
351 * @param string $type [optional] The relation type (child, parent, children, ...)
352 *
353 * @return boolean
354 */
355 public function hasRelation($itemName, $type = null)
356 {
357 $types = array_keys($this->relations);
358
359 if (in_array($type, $types))
360 {
361 $types = array($type);
362 }
363
364 foreach ($types as $type)
365 {
366 foreach ($this->relations[$type] as $key => $relations)
367 {
368 if ($itemName == $key)
369 {
370 return true;
371 }
372 }
373 }
374
375 return false;
376 }
377
378 /**
379 * Get the definition of a relation
380 *
381 * @param string $itemName The name of the relation to check
382 * @param string $type [optional] The relation type (child, parent, children, ...)
383 *
384 * @return array
385 *
386 * @throws RuntimeException When the relation is not found
387 */
388 public function getRelation($itemName, $type)
389 {
390 $types = array_keys($this->relations);
391
392 if (in_array($type, $types))
393 {
394 $types = array($type);
395 }
396
397 foreach ($types as $type)
398 {
399 foreach ($this->relations[$type] as $key => $relations)
400 {
401 if ($itemName == $key)
402 {
403 $temp = $relations;
404 $temp['type'] = $type;
405
406 return $temp;
407 }
408 }
409 }
410
411 throw new RuntimeException("Relation $itemName not found in table {$this->tableType}", 500);
412 }
413
414 /**
415 * Gets the item referenced by a named relation. You can optionally specify the type. Only single item relation
416 * types will be searched.
417 *
418 * @param string $itemName The name of the relation to use
419 * @param string $type [optional] The relation type (child, parent)
420 *
421 * @return FOFTable
422 *
423 * @throws RuntimeException If the named relation doesn't exist or isn't supposed to return single items
424 */
425 public function getRelatedItem($itemName, $type = null)
426 {
427 if (empty($type))
428 {
429 $relation = $this->getRelation($itemName, $type);
430 $type = $relation['type'];
431 }
432
433 switch ($type)
434 {
435 case 'parent':
436 return $this->getParent($itemName);
437 break;
438
439 case 'child':
440 return $this->getChild($itemName);
441 break;
442
443 default:
444 throw new RuntimeException("Invalid relation type $type for returning a single related item", 500);
445 break;
446 }
447 }
448
449 /**
450 * Gets the iterator for the items referenced by a named relation. You can optionally specify the type. Only
451 * multiple item relation types will be searched.
452 *
453 * @param string $itemName The name of the relation to use
454 * @param string $type [optional] The relation type (children, multiple)
455 *
456 * @return FOFDatabaseIterator
457 *
458 * @throws RuntimeException If the named relation doesn't exist or isn't supposed to return single items
459 */
460 public function getRelatedItems($itemName, $type = null)
461 {
462 if (empty($type))
463 {
464 $relation = $this->getRelation($itemName, $type);
465 $type = $relation['type'];
466 }
467
468 switch ($type)
469 {
470 case 'children':
471 return $this->getChildren($itemName);
472 break;
473
474 case 'multiple':
475 return $this->getMultiple($itemName);
476 break;
477
478 case 'siblings':
479 return $this->getSiblings($itemName);
480 break;
481
482 default:
483 throw new RuntimeException("Invalid relation type $type for returning a collection of related items", 500);
484 break;
485 }
486 }
487
488 /**
489 * Gets a parent item
490 *
491 * @param string $itemName [optional] The name of the relation to use, skip to use the default parent relation
492 *
493 * @return FOFTable
494 *
495 * @throws RuntimeException When the relation is not found
496 */
497 public function getParent($itemName = null)
498 {
499 if (empty($itemName))
500 {
501 $itemName = $this->defaultRelation['parent'];
502 }
503
504 if (empty($itemName))
505 {
506 throw new RuntimeException(sprintf('Default parent relation for %s not found', $this->table->getTableName()), 500);
507 }
508
509 if (!isset($this->relations['parent'][$itemName]))
510 {
511 throw new RuntimeException(sprintf('Parent relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
512 }
513
514 return $this->getTableFromRelation($this->relations['parent'][$itemName]);
515 }
516
517 /**
518 * Gets a child item
519 *
520 * @param string $itemName [optional] The name of the relation to use, skip to use the default child relation
521 *
522 * @return FOFTable
523 *
524 * @throws RuntimeException When the relation is not found
525 */
526 public function getChild($itemName = null)
527 {
528 if (empty($itemName))
529 {
530 $itemName = $this->defaultRelation['child'];
531 }
532
533 if (empty($itemName))
534 {
535 throw new RuntimeException(sprintf('Default child relation for %s not found', $this->table->getTableName()), 500);
536 }
537
538 if (!isset($this->relations['child'][$itemName]))
539 {
540 throw new RuntimeException(sprintf('Child relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
541 }
542
543 return $this->getTableFromRelation($this->relations['child'][$itemName]);
544 }
545
546 /**
547 * Gets an iterator for the children items
548 *
549 * @param string $itemName [optional] The name of the relation to use, skip to use the default children relation
550 *
551 * @return FOFDatabaseIterator
552 *
553 * @throws RuntimeException When the relation is not found
554 */
555 public function getChildren($itemName = null)
556 {
557 if (empty($itemName))
558 {
559 $itemName = $this->defaultRelation['children'];
560 }
561 if (empty($itemName))
562 {
563 throw new RuntimeException(sprintf('Default children relation for %s not found', $this->table->getTableName()), 500);
564 }
565
566 if (!isset($this->relations['children'][$itemName]))
567 {
568 throw new RuntimeException(sprintf('Children relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
569 }
570
571 return $this->getIteratorFromRelation($this->relations['children'][$itemName]);
572 }
573
574 /**
575 * Gets an iterator for the sibling items. This relation is inferred from the parent relation. It returns all
576 * elements on the same table which have the same parent.
577 *
578 * @param string $itemName [optional] The name of the relation to use, skip to use the default children relation
579 *
580 * @return FOFDatabaseIterator
581 *
582 * @throws RuntimeException When the relation is not found
583 */
584 public function getSiblings($itemName = null)
585 {
586 if (empty($itemName))
587 {
588 $itemName = $this->defaultRelation['parent'];
589 }
590 if (empty($itemName))
591 {
592 throw new RuntimeException(sprintf('Default siblings relation for %s not found', $this->table->getTableName()), 500);
593 }
594
595 if (!isset($this->relations['parent'][$itemName]))
596 {
597 throw new RuntimeException(sprintf('Sibling relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
598 }
599
600 // Get my table class
601 $tableName = $this->table->getTableName();
602 $tableName = str_replace('#__', '', $tableName);
603 $tableNameParts = explode('_', $tableName, 2);
604 $tableClass = ucfirst($tableNameParts[0]) . 'Table' . ucfirst(FOFInflector::singularize($tableNameParts[1]));
605
606 $parentRelation = $this->relations['parent'][$itemName];
607 $relation = array(
608 'tableClass' => $tableClass,
609 'localKey' => $parentRelation['localKey'],
610 'remoteKey' => $parentRelation['localKey'],
611 );
612
613 return $this->getIteratorFromRelation($relation);
614 }
615
616 /**
617 * Gets an iterator for the multiple items
618 *
619 * @param string $itemName [optional] The name of the relation to use, skip to use the default multiple relation
620 *
621 * @return FOFDatabaseIterator
622 *
623 * @throws RuntimeException When the relation is not found
624 */
625 public function getMultiple($itemName = null)
626 {
627 if (empty($itemName))
628 {
629 $itemName = $this->defaultRelation['multiple'];
630 }
631
632 if (empty($itemName))
633 {
634 throw new RuntimeException(sprintf('Default multiple relation for %s not found', $this->table->getTableName()), 500);
635 }
636
637 if (!isset($this->relations['multiple'][$itemName]))
638 {
639 throw new RuntimeException(sprintf('Multiple relation %s for %s not found', $itemName, $this->table->getTableName()), 500);
640 }
641
642 return $this->getIteratorFromRelation($this->relations['multiple'][$itemName]);
643 }
644
645 /**
646 * Returns a FOFTable object based on a given relation
647 *
648 * @param array $relation Indexed array holding relation definition.
649 * tableClass => name of the related table class
650 * localKey => name of the local key
651 * remoteKey => name of the remote key
652 *
653 * @return FOFTable
654 *
655 * @throws RuntimeException
656 * @throws InvalidArgumentException
657 */
658 protected function getTableFromRelation($relation)
659 {
660 // Sanity checks
661 if(
662 !isset($relation['tableClass']) || !isset($relation['remoteKey']) || !isset($relation['localKey']) ||
663 !$relation['tableClass'] || !$relation['remoteKey'] || !$relation['localKey']
664 )
665 {
666 throw new InvalidArgumentException('Missing array index for the '.__METHOD__.' method. Please check method signature', 500);
667 }
668
669 // Get a table object from the table class name
670 $tableClass = $relation['tableClass'];
671 $tableClassParts = FOFInflector::explode($tableClass);
672
673 if(count($tableClassParts) < 3)
674 {
675 throw new InvalidArgumentException('Invalid table class named. It should be something like FooTableBar');
676 }
677
678 $table = FOFTable::getInstance($tableClassParts[2], ucfirst($tableClassParts[0]) . ucfirst($tableClassParts[1]));
679
680 // Get the table name
681 $tableName = $table->getTableName();
682
683 // Get the remote and local key names
684 $remoteKey = $relation['remoteKey'];
685 $localKey = $relation['localKey'];
686
687 // Get the local key's value
688 $value = $this->table->$localKey;
689
690 // If there's no value for the primary key, let's stop here
691 if(!$value)
692 {
693 throw new RuntimeException('Missing value for the primary key of the table '.$this->table->getTableName(), 500);
694 }
695
696 // This is required to prevent one relation from killing the db cursor used in a different relation...
697 $oldDb = $this->table->getDbo();
698 $oldDb->disconnect(); // YES, WE DO NEED TO DISCONNECT BEFORE WE CLONE THE DB OBJECT. ARGH!
699 $db = clone $oldDb;
700
701 $query = $db->getQuery(true)
702 ->select('*')
703 ->from($db->qn($tableName))
704 ->where($db->qn($remoteKey) . ' = ' . $db->q($value));
705 $db->setQuery($query, 0, 1);
706
707 $data = $db->loadObject();
708
709 if (!is_object($data))
710 {
711 throw new RuntimeException(sprintf('Cannot load item from relation against table %s column %s', $tableName, $remoteKey), 500);
712 }
713
714 $table->bind($data);
715
716 return $table;
717 }
718
719 /**
720 * Returns a FOFDatabaseIterator based on a given relation
721 *
722 * @param array $relation Indexed array holding relation definition.
723 * tableClass => name of the related table class
724 * localKey => name of the local key
725 * remoteKey => name of the remote key
726 * pivotTable => name of the pivot table (optional)
727 * theirPivotKey => name of the remote key in the pivot table (mandatory if pivotTable is set)
728 * ourPivotKey => name of our key in the pivot table (mandatory if pivotTable is set)
729 *
730 * @return FOFDatabaseIterator
731 *
732 * @throws RuntimeException
733 * @throws InvalidArgumentException
734 */
735 protected function getIteratorFromRelation($relation)
736 {
737 // Sanity checks
738 if(
739 !isset($relation['tableClass']) || !isset($relation['remoteKey']) || !isset($relation['localKey']) ||
740 !$relation['tableClass'] || !$relation['remoteKey'] || !$relation['localKey']
741 )
742 {
743 throw new InvalidArgumentException('Missing array index for the '.__METHOD__.' method. Please check method signature', 500);
744 }
745
746 if(array_key_exists('pivotTable', $relation))
747 {
748 if(
749 !isset($relation['theirPivotKey']) || !isset($relation['ourPivotKey']) ||
750 !$relation['pivotTable'] || !$relation['theirPivotKey'] || !$relation['ourPivotKey']
751 )
752 {
753 throw new InvalidArgumentException('Missing array index for the '.__METHOD__.' method. Please check method signature', 500);
754 }
755 }
756
757 // Get a table object from the table class name
758 $tableClass = $relation['tableClass'];
759 $tableClassParts = FOFInflector::explode($tableClass);
760
761 if(count($tableClassParts) < 3)
762 {
763 throw new InvalidArgumentException('Invalid table class named. It should be something like FooTableBar');
764 }
765
766 $table = FOFTable::getInstance($tableClassParts[2], ucfirst($tableClassParts[0]) . ucfirst($tableClassParts[1]));
767
768 // Get the table name
769 $tableName = $table->getTableName();
770
771 // Get the remote and local key names
772 $remoteKey = $relation['remoteKey'];
773 $localKey = $relation['localKey'];
774
775 // Get the local key's value
776 $value = $this->table->$localKey;
777
778 // If there's no value for the primary key, let's stop here
779 if(!$value)
780 {
781 throw new RuntimeException('Missing value for the primary key of the table '.$this->table->getTableName(), 500);
782 }
783
784 // This is required to prevent one relation from killing the db cursor used in a different relation...
785 $oldDb = $this->table->getDbo();
786 $oldDb->disconnect(); // YES, WE DO NEED TO DISCONNECT BEFORE WE CLONE THE DB OBJECT. ARGH!
787 $db = clone $oldDb;
788
789 // Begin the query
790 $query = $db->getQuery(true)
791 ->select('*')
792 ->from($db->qn($tableName));
793
794 // Do we have a pivot table?
795 $hasPivot = array_key_exists('pivotTable', $relation);
796
797 // If we don't have pivot it's a straightforward query
798 if (!$hasPivot)
799 {
800 $query->where($db->qn($remoteKey) . ' = ' . $db->q($value));
801 }
802 // If we have a pivot table we have to do a subquery
803 else
804 {
805 $subQuery = $db->getQuery(true)
806 ->select($db->qn($relation['theirPivotKey']))
807 ->from($db->qn($relation['pivotTable']))
808 ->where($db->qn($relation['ourPivotKey']) . ' = ' . $db->q($value));
809 $query->where($db->qn($remoteKey) . ' IN (' . $subQuery . ')');
810 }
811
812 $db->setQuery($query);
813
814 $cursor = $db->execute();
815
816 $iterator = FOFDatabaseIterator::getIterator($db->name, $cursor, null, $tableClass);
817
818 return $iterator;
819 }
820
821 /**
822 * Add any bespoke relation which doesn't involve a pivot table.
823 *
824 * @param string $relationType The type of the relationship (parent, child, children)
825 * @param string $itemName is how it will be known locally to the getRelatedItems method
826 * @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
827 * @param string $localKey is the column containing our side of the FK relation, default: componentname_itemname_id
828 * @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
829 * @param boolean $default is this the default children relationship?
830 *
831 * @return void
832 */
833 protected function addBespokeSimpleRelation($relationType, $itemName, $tableClass, $localKey, $remoteKey, $default)
834 {
835 $ourPivotKey = null;
836 $theirPivotKey = null;
837 $pivotTable = null;
838
839 $this->normaliseParameters(false, $itemName, $tableClass, $localKey, $remoteKey, $ourPivotKey, $theirPivotKey, $pivotTable);
840
841 $this->relations[$relationType][$itemName] = array(
842 'tableClass' => $tableClass,
843 'localKey' => $localKey,
844 'remoteKey' => $remoteKey,
845 );
846
847 if ($default)
848 {
849 $this->defaultRelation[$relationType] = $itemName;
850 }
851 }
852
853 /**
854 * Add any bespoke relation which involves a pivot table.
855 *
856 * @param string $relationType The type of the relationship (multiple)
857 * @param string $itemName is how it will be known locally to the getRelatedItems method
858 * @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
859 * @param string $localKey is the column containing our side of the FK relation, default: componentname_itemname_id
860 * @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
861 * @param string $ourPivotKey is the column containing our side of the FK relation in the pivot table, default: $localKey
862 * @param string $theirPivotKey is the column containing the other table's side of the FK relation in the pivot table, default $remoteKey
863 * @param string $pivotTable is the name of the glue (pivot) table, default: #__componentname_thisclassname_itemname with plural items (e.g. #__foobar_users_roles)
864 * @param boolean $default is this the default children relationship?
865 *
866 * @return void
867 */
868 protected function addBespokePivotRelation($relationType, $itemName, $tableClass, $localKey, $remoteKey, $ourPivotKey, $theirPivotKey, $pivotTable, $default)
869 {
870 $this->normaliseParameters(true, $itemName, $tableClass, $localKey, $remoteKey, $ourPivotKey, $theirPivotKey, $pivotTable);
871
872 $this->relations[$relationType][$itemName] = array(
873 'tableClass' => $tableClass,
874 'localKey' => $localKey,
875 'remoteKey' => $remoteKey,
876 'ourPivotKey' => $ourPivotKey,
877 'theirPivotKey' => $theirPivotKey,
878 'pivotTable' => $pivotTable,
879 );
880
881 if ($default)
882 {
883 $this->defaultRelation[$relationType] = $itemName;
884 }
885 }
886
887 /**
888 * Normalise the parameters of a relation, guessing missing values
889 *
890 * @param boolean $pivot Is this a many to many relation involving a pivot table?
891 * @param string $itemName is how it will be known locally to the getRelatedItems method (plural)
892 * @param string $tableClass if skipped it is defined automatically as ComponentnameTableItemname
893 * @param string $localKey is the column containing our side of the FK relation, default: componentname_itemname_id
894 * @param string $remoteKey is the remote table's FK column, default: componentname_itemname_id
895 * @param string $ourPivotKey is the column containing our side of the FK relation in the pivot table, default: $localKey
896 * @param string $theirPivotKey is the column containing the other table's side of the FK relation in the pivot table, default $remoteKey
897 * @param string $pivotTable is the name of the glue (pivot) table, default: #__componentname_thisclassname_itemname with plural items (e.g. #__foobar_users_roles)
898 *
899 * @return void
900 */
901 protected function normaliseParameters($pivot = false, &$itemName, &$tableClass, &$localKey, &$remoteKey, &$ourPivotKey, &$theirPivotKey, &$pivotTable)
902 {
903 // Get a default table class if none is provided
904 if (empty($tableClass))
905 {
906 $tableClassParts = explode('_', $itemName, 3);
907
908 if (count($tableClassParts) == 1)
909 {
910 array_unshift($tableClassParts, $this->componentName);
911 }
912
913 if ($tableClassParts[0] == 'joomla')
914 {
915 $tableClassParts[0] = 'J';
916 }
917
918 $tableClass = ucfirst($tableClassParts[0]) . 'Table' . ucfirst(FOFInflector::singularize($tableClassParts[1]));
919 }
920
921 // Make sure we have both a local and remote key
922 if (empty($localKey) && empty($remoteKey))
923 {
924 // WARNING! If we have a pivot table, this behavior is wrong!
925 // Infact if we have `parts` and `groups` the local key should be foobar_part_id and the remote one foobar_group_id.
926 // However, this isn't a real issue because:
927 // 1. we have no way to detect the local key of a multiple relation
928 // 2. this scenario never happens, since, in this class, if we're adding a multiple relation we always supply the local key
929 $tableClassParts = FOFInflector::explode($tableClass);
930 $localKey = $tableClassParts[0] . '_' . $tableClassParts[2] . '_id';
931 $remoteKey = $localKey;
932 }
933 elseif (empty($localKey) && !empty($remoteKey))
934 {
935 $localKey = $remoteKey;
936 }
937 elseif (!empty($localKey) && empty($remoteKey))
938 {
939 if($pivot)
940 {
941 $tableClassParts = FOFInflector::explode($tableClass);
942 $remoteKey = $tableClassParts[0] . '_' . $tableClassParts[2] . '_id';
943 }
944 else
945 {
946 $remoteKey = $localKey;
947 }
948 }
949
950 // If we don't have a pivot table nullify the relevant variables and return
951 if (!$pivot)
952 {
953 $ourPivotKey = null;
954 $theirPivotKey = null;
955 $pivotTable = null;
956
957 return;
958 }
959
960 if (empty($ourPivotKey))
961 {
962 $ourPivotKey = $localKey;
963 }
964
965 if (empty($theirPivotKey))
966 {
967 $theirPivotKey = $remoteKey;
968 }
969
970 if (empty($pivotTable))
971 {
972 $pivotTable = '#__' . strtolower($this->componentName) . '_' .
973 strtolower(FOFInflector::pluralize($this->tableType)) . '_';
974
975 $itemNameParts = explode('_', $itemName);
976 $lastPart = array_pop($itemNameParts);
977 $pivotTable .= strtolower($lastPart);
978 }
979 }
980
981 /**
982 * Normalises the format of a relation name
983 *
984 * @param string $itemName The raw relation name
985 * @param boolean $pluralise Should I pluralise the name? If not, I will singularise it
986 *
987 * @return string The normalised relation key name
988 */
989 protected function normaliseItemName($itemName, $pluralise = false)
990 {
991 // Explode the item name
992 $itemNameParts = explode('_', $itemName);
993
994 // If we have multiple parts the first part is considered to be the component name
995 if (count($itemNameParts) > 1)
996 {
997 $prefix = array_shift($itemNameParts);
998 }
999 else
1000 {
1001 $prefix = null;
1002 }
1003
1004 // If we still have multiple parts we need to pluralise/singularise the last part and join everything in
1005 // CamelCase format
1006 if (count($itemNameParts) > 1)
1007 {
1008 $name = array_pop($itemNameParts);
1009 $name = $pluralise ? FOFInflector::pluralize($name) : FOFInflector::singularize($name);
1010 $itemNameParts[] = $name;
1011
1012 $itemName = FOFInflector::implode($itemNameParts);
1013 }
1014 // Otherwise we singularise/pluralise the remaining part
1015 else
1016 {
1017 $name = array_pop($itemNameParts);
1018 $itemName = $pluralise ? FOFInflector::pluralize($name) : FOFInflector::singularize($name);
1019 }
1020
1021 if (!empty($prefix))
1022 {
1023 $itemName = $prefix . '_' . $itemName;
1024 }
1025
1026 return $itemName;
1027 }
1028 }