Merge pull request #6548 from eileenmcnaughton/CRM-16512
[civicrm-core.git] / CRM / Contact / BAO / GroupNesting.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright U.S. PIRG Education Fund (c) 2007 |
7 | Licensed to CiviCRM under the Academic Free License version 3.0. |
8 +--------------------------------------------------------------------+
9 | This file is a part of CiviCRM. |
10 | |
11 | CiviCRM is free software; you can copy, modify, and distribute it |
12 | under the terms of the GNU Affero General Public License |
13 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | |
15 | CiviCRM is distributed in the hope that it will be useful, but |
16 | WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
18 | See the GNU Affero General Public License for more details. |
19 | |
20 | You should have received a copy of the GNU Affero General Public |
21 | License and the CiviCRM Licensing Exception along |
22 | with this program; if not, contact CiviCRM LLC |
23 | at info[AT]civicrm[DOT]org. If you have questions about the |
24 | GNU Affero General Public License or the licensing of CiviCRM, |
25 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
26 +--------------------------------------------------------------------+
27 */
28
29 /**
30 *
31 * @package CRM
32 * @copyright U.S. PIRG 2007
33 * $Id$
34 *
35 */
36 class CRM_Contact_BAO_GroupNesting extends CRM_Contact_DAO_GroupNesting implements Iterator {
37
38 static $_sortOrder = 'ASC';
39
40 private $_current;
41
42 private $_parentStack = array();
43
44 private $_lastParentlessGroup;
45
46 private $_styleLabels;
47
48 private $_styleIndent;
49
50 private $_alreadyStyled = FALSE;
51
52 /**
53 * Class constructor.
54 *
55 * @param bool $styleLabels
56 * @param string $styleIndent
57 */
58 public function __construct($styleLabels = FALSE, $styleIndent = "&nbsp;--&nbsp;") {
59 parent::__construct();
60 $this->_styleLabels = $styleLabels;
61 $this->_styleIndent = $styleIndent;
62 }
63
64 /**
65 * @param $sortOrder
66 */
67 public function setSortOrder($sortOrder) {
68 switch ($sortOrder) {
69 case 'ASC':
70 case 'DESC':
71 if ($sortOrder != self::$_sortOrder) {
72 self::$_sortOrder = $sortOrder;
73 $this->rewind();
74 }
75 break;
76
77 default:
78 // spit out some error, someday
79 }
80 }
81
82 /**
83 * @return string
84 */
85 public function getSortOrder() {
86 return self::$_sortOrder;
87 }
88
89 /**
90 * @return int
91 */
92 public function getCurrentNestingLevel() {
93 return count($this->_parentStack);
94 }
95
96 /**
97 * Go back to the first element in the group nesting graph,
98 * which is the first group (according to _sortOrder) that
99 * has no parent groups
100 */
101 public function rewind() {
102 $this->_parentStack = array();
103 // calling _getNextParentlessGroup w/ no arguments
104 // makes it return the first parentless group
105 $firstGroup = $this->_getNextParentlessGroup();
106 $this->_current = $firstGroup;
107 $this->_lastParentlessGroup = $firstGroup;
108 $this->_alreadyStyled = FALSE;
109 }
110
111 /**
112 * @return mixed
113 */
114 public function current() {
115 if ($this->_styleLabels &&
116 $this->valid() &&
117 !$this->_alreadyStyled
118 ) {
119 $styledGroup = clone($this->_current);
120 $nestingLevel = $this->getCurrentNestingLevel();
121 $indent = '';
122 while ($nestingLevel--) {
123 $indent .= $this->_styleIndent;
124 }
125 $styledGroup->title = $indent . $styledGroup->title;
126
127 $this->_current = &$styledGroup;
128 $this->_alreadyStyled = TRUE;
129 }
130 return $this->_current;
131 }
132
133 /**
134 * @return string
135 */
136 public function key() {
137 $group = &$this->_current;
138 $ids = array();
139 foreach ($this->_parentStack as $parentGroup) {
140 $ids[] = $parentGroup->id;
141 }
142 $key = implode('-', $ids);
143 if (strlen($key) > 0) {
144 $key .= '-';
145 }
146 $key .= $group->id;
147 return $key;
148 }
149
150 /**
151 * @return CRM_Contact_BAO_Group|null
152 */
153 public function next() {
154 $currentGroup = &$this->_current;
155 $childGroup = $this->_getNextChildGroup($currentGroup);
156 if ($childGroup) {
157 $nextGroup = &$childGroup;
158 $this->_parentStack[] = &$this->_current;
159 }
160 else {
161 $nextGroup = $this->_getNextSiblingGroup($currentGroup);
162 if (!$nextGroup) {
163 // no sibling, find an ancestor w/ a sibling
164 for (;;) {
165 // since we pop this array everytime, we should be
166 // reasonably safe from infinite loops, I think :)
167 $ancestor = array_pop($this->_parentStack);
168 $this->_current = &$ancestor;
169 if ($ancestor == NULL) {
170 break;
171 }
172 $nextGroup = $this->_getNextSiblingGroup($ancestor);
173 if ($nextGroup) {
174 break;
175 }
176 }
177 }
178 }
179 $this->_current = &$nextGroup;
180 $this->_alreadyStyled = FALSE;
181 return $nextGroup;
182 }
183
184 /**
185 * @return bool
186 */
187 public function valid() {
188 if ($this->_current) {
189 return TRUE;
190 }
191 else {
192 return FALSE;
193 }
194 }
195
196 /**
197 * @param null $group
198 *
199 * @return CRM_Contact_BAO_Group|null
200 */
201 public function _getNextParentlessGroup(&$group = NULL) {
202 $lastParentlessGroup = $this->_lastParentlessGroup;
203 $nextGroup = new CRM_Contact_BAO_Group();
204 $nextGroup->order_by = 'title ' . self::$_sortOrder;
205 $nextGroup->find();
206 if ($group == NULL) {
207 $sawLast = TRUE;
208 }
209 else {
210 $sawLast = FALSE;
211 }
212 while ($nextGroup->fetch()) {
213 if (!self::hasParentGroups($nextGroup->id) && $sawLast) {
214 return $nextGroup;
215 }
216 elseif ($lastParentlessGroup->id == $nextGroup->id) {
217 $sawLast = TRUE;
218 }
219 }
220 return NULL;
221 }
222
223 /**
224 * @param $parentGroup
225 * @param null $group
226 *
227 * @return CRM_Contact_BAO_Group|null
228 */
229 public function _getNextChildGroup(&$parentGroup, &$group = NULL) {
230 $children = self::getChildGroupIds($parentGroup->id);
231 if (count($children) > 0) {
232 // we have child groups, so get the first one based on _sortOrder
233 $childGroup = new CRM_Contact_BAO_Group();
234 $cgQuery = "SELECT * FROM civicrm_group WHERE id IN (" . implode(',', $children) . ") ORDER BY title " . self::$_sortOrder;
235 $childGroup->query($cgQuery);
236 $currentGroup = &$this->_current;
237 if ($group == NULL) {
238 $sawLast = TRUE;
239 }
240 else {
241 $sawLast = FALSE;
242 }
243 while ($childGroup->fetch()) {
244 if ($sawLast) {
245 return $childGroup;
246 }
247 elseif ($currentGroup->id === $childGroup->id) {
248 $sawLast = TRUE;
249 }
250 }
251 }
252 return NULL;
253 }
254
255 /**
256 * @param $group
257 *
258 * @return CRM_Contact_BAO_Group|null
259 */
260 public function _getNextSiblingGroup(&$group) {
261 $parentGroup = end($this->_parentStack);
262 if ($parentGroup) {
263 $nextGroup = $this->_getNextChildGroup($parentGroup, $group);
264 return $nextGroup;
265 }
266 else {
267 /* if we get here, it could be because we're out of siblings
268 * (in which case we return null) or because we're at the
269 * top level groups which do not have parents but may still
270 * have siblings, so check for that first.
271 */
272
273 $nextGroup = $this->_getNextParentlessGroup($group);
274 if ($nextGroup) {
275 $this->_lastParentlessGroup = $nextGroup;
276 return $nextGroup;
277 }
278 return NULL;
279 }
280 }
281
282 /**
283 * Adds a new child group identified by $childGroupId to the group
284 * identified by $groupId
285 *
286 * @param int $parentID
287 * Id of the group to add the child to.
288 * @param int $childID
289 * Id of the new child group.
290 *
291 *
292 * @return void
293 */
294 public static function add($parentID, $childID) {
295 // TODO: Add checks here to make sure invalid nests can't be created
296 $dao = new CRM_Contact_DAO_GroupNesting();
297 $query = "REPLACE INTO civicrm_group_nesting (child_group_id, parent_group_id) VALUES ($childID,$parentID);";
298 $dao->query($query);
299 }
300
301 /**
302 * Removes a child group identified by $childGroupId from the group
303 * identified by $groupId; does not delete child group, just the
304 * association between the two
305 *
306 * @param $parentID
307 * The id of the group to remove the child from.
308 * @param $childID
309 * The id of the child group being removed.
310 *
311 * @return void
312 */
313 public static function remove($parentID, $childID) {
314 $dao = new CRM_Contact_DAO_GroupNesting();
315 $query = "DELETE FROM civicrm_group_nesting WHERE child_group_id = $childID AND parent_group_id = $parentID";
316 $dao->query($query);
317 }
318
319 /**
320 * Removes associations where a child group is identified by $childGroupId from the group
321 * identified by $groupId; does not delete child group, just the
322 * association between the two
323 *
324 * @param int $childID
325 * The id of the child group being removed.
326 *
327 * @return void
328 */
329 public static function removeAllParentForChild($childID) {
330 $dao = new CRM_Contact_DAO_GroupNesting();
331 $query = "DELETE FROM civicrm_group_nesting WHERE child_group_id = $childID";
332 $dao->query($query);
333 }
334
335 /**
336 * Returns true if the association between parent and child is present,
337 * false otherwise.
338 *
339 * @param $parentID
340 * The parent id of the association.
341 * @param $childID
342 * The child id of the association.
343 *
344 * @return bool
345 * True if association is found, false otherwise.
346 */
347 public static function isParentChild($parentID, $childID) {
348 $dao = new CRM_Contact_DAO_GroupNesting();
349 $query = "SELECT id FROM civicrm_group_nesting WHERE child_group_id = $childID AND parent_group_id = $parentID";
350 $dao->query($query);
351 if ($dao->fetch()) {
352 return TRUE;
353 }
354 return FALSE;
355 }
356
357 /**
358 * Returns true if if the given groupId has 1 or more child groups,
359 * false otherwise.
360 *
361 * @param $groupId
362 * The id of the group to check for child groups.
363 *
364 * @return bool
365 * True if 1 or more child groups are found, false otherwise.
366 */
367 public static function hasChildGroups($groupId) {
368 $dao = new CRM_Contact_DAO_GroupNesting();
369 $query = "SELECT child_group_id FROM civicrm_group_nesting WHERE parent_group_id = $groupId LIMIT 1";
370 //print $query . "\n<br><br>";
371 $dao->query($query);
372 if ($dao->fetch()) {
373 return TRUE;
374 }
375 return FALSE;
376 }
377
378 /**
379 * Returns true if the given groupId has 1 or more parent groups,
380 * false otherwise.
381 *
382 * @param $groupId
383 * The id of the group to check for parent groups.
384 *
385 * @return bool
386 * True if 1 or more parent groups are found, false otherwise.
387 */
388 public static function hasParentGroups($groupId) {
389 $dao = new CRM_Contact_DAO_GroupNesting();
390 $query = "SELECT parent_group_id FROM civicrm_group_nesting WHERE child_group_id = $groupId LIMIT 1";
391 $dao->query($query);
392 if ($dao->fetch()) {
393 return TRUE;
394 }
395 return FALSE;
396 }
397
398 /**
399 * Returns true if checkGroupId is a parent of one of the groups in
400 * groupIds, false otherwise.
401 *
402 * @param array $groupIds
403 * of group ids (or one group id) to serve as the starting point.
404 * @param $checkGroupId
405 * The group id to check if it is a parent of the $groupIds group(s).
406 *
407 * @return bool
408 * True if $checkGroupId points to a group that is a parent of one of the $groupIds groups, false otherwise.
409 */
410 public static function isParentGroup($groupIds, $checkGroupId) {
411 if (!is_array($groupIds)) {
412 $groupIds = array($groupIds);
413 }
414 $dao = new CRM_Contact_DAO_GroupNesting();
415 $query = "SELECT parent_group_id FROM civicrm_group_nesting WHERE child_group_id IN (" . implode(',', $groupIds) . ")";
416 $dao->query($query);
417 while ($dao->fetch()) {
418 $parentGroupId = $dao->parent_group_id;
419 if ($parentGroupId == $checkGroupId) {
420 /* print "One of these: <pre>";
421 print_r($groupIds);
422 print "</pre> has groupId $checkGroupId as an ancestor.<br/>"; */
423
424 return TRUE;
425 }
426 }
427 return FALSE;
428 }
429
430 /**
431 * Returns true if checkGroupId is a child of one of the groups in
432 * groupIds, false otherwise.
433 *
434 * @param array $groupIds
435 * of group ids (or one group id) to serve as the starting point.
436 * @param $checkGroupId
437 * The group id to check if it is a child of the $groupIds group(s).
438 *
439 * @return bool
440 * True if $checkGroupId points to a group that is a child of one of the $groupIds groups, false otherwise.
441 */
442 public static function isChildGroup($groupIds, $checkGroupId) {
443
444 if (!is_array($groupIds)) {
445 $groupIds = array($groupIds);
446 }
447 $dao = new CRM_Contact_DAO_GroupNesting();
448 $query = "SELECT child_group_id FROM civicrm_group_nesting WHERE parent_group_id IN (" . implode(',', $groupIds) . ")";
449 //print $query;
450 $dao->query($query);
451 while ($dao->fetch()) {
452 $childGroupId = $dao->child_group_id;
453 if ($childGroupId == $checkGroupId) {
454 /* print "One of these: <pre>";
455 print_r($groupIds);
456 print "</pre> has groupId $checkGroupId as a descendent.<br/><br/>"; */
457
458 return TRUE;
459 }
460 }
461 return FALSE;
462 }
463
464 /**
465 * Returns true if checkGroupId is an ancestor of one of the groups in
466 * groupIds, false otherwise.
467 *
468 * @param array $groupIds
469 * of group ids (or one group id) to serve as the starting point.
470 * @param $checkGroupId
471 * The group id to check if it is an ancestor of the $groupIds group(s).
472 *
473 * @return bool
474 * True if $checkGroupId points to a group that is an ancestor of one of the $groupIds groups, false otherwise.
475 */
476 public static function isAncestorGroup($groupIds, $checkGroupId) {
477 if (!is_array($groupIds)) {
478 $groupIds = array($groupIds);
479 }
480 $dao = new CRM_Contact_DAO_GroupNesting();
481 $query = "SELECT parent_group_id FROM civicrm_group_nesting WHERE child_group_id IN (" . implode(',', $groupIds) . ")";
482 $dao->query($query);
483 $nextGroupIds = array();
484 $gotAtLeastOneResult = FALSE;
485 while ($dao->fetch()) {
486 $gotAtLeastOneResult = TRUE;
487 $parentGroupId = $dao->parent_group_id;
488 if ($parentGroupId == $checkGroupId) {
489 /* print "One of these: <pre>";
490 print_r($groupIds);
491 print "</pre> has groupId $checkGroupId as an ancestor.<br/>"; */
492
493 return TRUE;
494 }
495 $nextGroupIds[] = $parentGroupId;
496 }
497 if ($gotAtLeastOneResult) {
498 return self::isAncestorGroup($nextGroupIds, $checkGroupId);
499 }
500 else {
501 return FALSE;
502 }
503 }
504
505 /**
506 * Returns true if checkGroupId is a descendent of one of the groups in
507 * groupIds, false otherwise.
508 *
509 * @param array $groupIds
510 * of group ids (or one group id) to serve as the starting point.
511 * @param $checkGroupId
512 * The group id to check if it is a descendent of the $groupIds group(s).
513 *
514 * @return bool
515 * True if $checkGroupId points to a group that is a descendent of one of the $groupIds groups, false otherwise.
516 */
517 public static function isDescendentGroup($groupIds, $checkGroupId) {
518 if (!is_array($groupIds)) {
519 $groupIds = array($groupIds);
520 }
521 $dao = new CRM_Contact_DAO_GroupNesting();
522 $query = "SELECT child_group_id FROM civicrm_group_nesting WHERE parent_group_id IN (" . implode(',', $groupIds) . ")";
523 $dao->query($query);
524 $nextGroupIds = array();
525 $gotAtLeastOneResult = FALSE;
526 while ($dao->fetch()) {
527 $gotAtLeastOneResult = TRUE;
528 $childGroupId = $dao->child_group_id;
529 if ($childGroupId == $checkGroupId) {
530 /* print "One of these: <pre>";
531 print_r($groupIds);
532 print "</pre> has groupId $checkGroupId as a descendent.<br/><br/>"; */
533
534 return TRUE;
535 }
536 $nextGroupIds[] = $childGroupId;
537 }
538 if ($gotAtLeastOneResult) {
539 return self::isDescendentGroup($nextGroupIds, $checkGroupId);
540 }
541 else {
542 return FALSE;
543 }
544 }
545
546 /**
547 * Returns array of group ids of ancestor groups of the specified group.
548 *
549 * @param array $groupIds
550 * An array of valid group ids (passed by reference).
551 *
552 * @param bool $includeSelf
553 *
554 * @return array
555 * List of groupIds that represent the requested group and its ancestors
556 */
557 public static function getAncestorGroupIds($groupIds, $includeSelf = TRUE) {
558 if (!is_array($groupIds)) {
559 $groupIds = array($groupIds);
560 }
561 $dao = new CRM_Contact_DAO_GroupNesting();
562 $query = "SELECT parent_group_id, child_group_id
563 FROM civicrm_group_nesting
564 WHERE child_group_id IN (" . implode(',', $groupIds) . ")";
565 $dao->query($query);
566 $tmpGroupIds = array();
567 $parentGroupIds = array();
568 if ($includeSelf) {
569 $parentGroupIds = $groupIds;
570 }
571 while ($dao->fetch()) {
572 // make sure we're not following any cyclical references
573 if (!array_key_exists($dao->child_group_id, $parentGroupIds) && $dao->parent_group_id != $groupIds[0]) {
574 $tmpGroupIds[] = $dao->parent_group_id;
575 }
576 }
577 if (!empty($tmpGroupIds)) {
578 $newParentGroupIds = self::getAncestorGroupIds($tmpGroupIds);
579 $parentGroupIds = array_merge($parentGroupIds, $newParentGroupIds);
580 }
581 return $parentGroupIds;
582 }
583
584 /**
585 * Returns array of ancestor groups of the specified group.
586 *
587 * @param array $groupIds
588 * An array of valid group ids (passed by reference).
589 *
590 * @param bool $includeSelf
591 * @return array
592 * List of ancestor groups
593 */
594 public static function getAncestorGroups($groupIds, $includeSelf = TRUE) {
595 $groupIds = self::getAncestorGroupIds($groupIds, $includeSelf);
596 $params['id'] = $groupIds;
597 return CRM_Contact_BAO_Group::getGroups($params);
598 }
599
600 /**
601 * Returns array of group ids of child groups of the specified group.
602 *
603 * @param array $groupIds
604 * An array of valid group ids (passed by reference).
605 *
606 * @return array
607 * List of groupIds that represent the requested group and its children
608 */
609 public static function getChildGroupIds($groupIds) {
610 if (!is_array($groupIds)) {
611 $groupIds = array($groupIds);
612 }
613 $dao = new CRM_Contact_DAO_GroupNesting();
614 $query = "SELECT child_group_id FROM civicrm_group_nesting WHERE parent_group_id IN (" . implode(',', $groupIds) . ")";
615 $dao->query($query);
616 $childGroupIds = array();
617 while ($dao->fetch()) {
618 $childGroupIds[] = $dao->child_group_id;
619 }
620 return $childGroupIds;
621 }
622
623 /**
624 * Returns array of group ids of parent groups of the specified group.
625 *
626 * @param array $groupIds
627 * An array of valid group ids (passed by reference).
628 *
629 * @return array
630 * List of groupIds that represent the requested group and its parents
631 */
632 public static function getParentGroupIds($groupIds) {
633 if (!is_array($groupIds)) {
634 $groupIds = array($groupIds);
635 }
636 $dao = new CRM_Contact_DAO_GroupNesting();
637 $query = "SELECT parent_group_id FROM civicrm_group_nesting WHERE child_group_id IN (" . implode(',', $groupIds) . ")";
638 $dao->query($query);
639 $parentGroupIds = array();
640 while ($dao->fetch()) {
641 $parentGroupIds[] = $dao->parent_group_id;
642 }
643 return $parentGroupIds;
644 }
645
646 /**
647 * Returns array of group ids of descendent groups of the specified group.
648 *
649 * @param array $groupIds
650 * An array of valid group ids (passed by reference).
651 *
652 * @param bool $includeSelf
653 * @return array
654 * List of groupIds that represent the requested group and its descendents
655 */
656 public static function getDescendentGroupIds($groupIds, $includeSelf = TRUE) {
657 if (!is_array($groupIds)) {
658 $groupIds = array($groupIds);
659 }
660 $dao = new CRM_Contact_DAO_GroupNesting();
661 $query = "SELECT child_group_id, parent_group_id FROM civicrm_group_nesting WHERE parent_group_id IN (" . implode(',', $groupIds) . ")";
662 $dao->query($query);
663 $tmpGroupIds = array();
664 $childGroupIds = array();
665 if ($includeSelf) {
666 $childGroupIds = $groupIds;
667 }
668 while ($dao->fetch()) {
669 // make sure we're not following any cyclical references
670 if (!array_key_exists($dao->parent_group_id, $childGroupIds) && $dao->child_group_id != $groupIds[0]) {
671 $tmpGroupIds[] = $dao->child_group_id;
672 }
673 }
674 if (!empty($tmpGroupIds)) {
675 $newChildGroupIds = self::getDescendentGroupIds($tmpGroupIds);
676 $childGroupIds = array_merge($childGroupIds, $newChildGroupIds);
677 }
678 return $childGroupIds;
679 }
680
681 /**
682 * Returns array of descendent groups of the specified group.
683 *
684 * @param array $groupIds
685 * An array of valid group ids
686 *
687 * @param bool $includeSelf
688 * @return array
689 * List of descendent groups
690 */
691 public static function getDescendentGroups($groupIds, $includeSelf = TRUE) {
692 $groupIds = self::getDescendentGroupIds($groupIds, $includeSelf);
693 $params['id'] = $groupIds;
694 return CRM_Contact_BAO_Group::getGroups($params);
695 }
696
697 /**
698 * Returns array of group ids of valid potential child groups of the specified group.
699 *
700 * @param $groupId
701 * The group id to get valid potential children for.
702 *
703 * @return array
704 * List of groupIds that represent the valid potential children of the group
705 */
706 public static function getPotentialChildGroupIds($groupId) {
707 $groups = CRM_Contact_BAO_Group::getGroups();
708 $potentialChildGroupIds = array();
709 foreach ($groups as $group) {
710 $potentialChildGroupId = $group->id;
711 // print "Checking if $potentialChildGroupId is a descendent/ancestor of $groupId<br/><br/>";
712 if (!self::isDescendentGroup($groupId, $potentialChildGroupId) &&
713 !self::isAncestorGroup($groupId, $potentialChildGroupId) &&
714 $potentialChildGroupId != $groupId
715 ) {
716 $potentialChildGroupIds[] = $potentialChildGroupId;
717 }
718 }
719 return $potentialChildGroupIds;
720 }
721
722 /**
723 * @param int $contactId
724 * @param int $parentGroupId
725 *
726 * @return array
727 */
728 public static function getContainingGroups($contactId, $parentGroupId) {
729 $groups = CRM_Contact_BAO_Group::getGroups();
730 $containingGroups = array();
731 foreach ($groups as $group) {
732 if (self::isDescendentGroup($parentGroupId, $group->id)) {
733 $members = CRM_Contact_BAO_Group::getMember($group->id);
734 if ($members[$contactId]) {
735 $containingGroups[] = $group->title;
736 }
737 }
738 }
739
740 return $containingGroups;
741 }
742
743 }