Ref - Use writeRecord and hooks in GroupContact BAO
[civicrm-core.git] / CRM / Contact / BAO / GroupContact.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Core\Event\PostEvent;
13
14 /**
15 *
16 * @package CRM
17 * @copyright CiviCRM LLC https://civicrm.org/licensing
18 */
19 class CRM_Contact_BAO_GroupContact extends CRM_Contact_DAO_GroupContact implements \Civi\Test\HookInterface {
20
21 /**
22 * Deprecated add function
23 *
24 * @param array $params
25 *
26 * @return CRM_Contact_DAO_GroupContact
27 * @throws \CRM_Core_Exception
28 *
29 * @deprecated
30 */
31 public static function add(array $params): CRM_Contact_DAO_GroupContact {
32 return self::writeRecord($params);
33 }
34
35 /**
36 * Callback for hook_civicrm_post().
37 *
38 * @param \Civi\Core\Event\PostEvent $event
39 *
40 * @noinspection PhpUnused
41 * @noinspection UnknownInspectionInspection
42 */
43 public static function self_hook_civicrm_post(PostEvent $event): void {
44 if (is_object($event->object) && in_array($event->action, ['create', 'edit'], TRUE)) {
45 // Lookup existing info for the sake of subscription history
46 if ($event->action === 'edit') {
47 $event->object->find(TRUE);
48 }
49 $params = $event->object->toArray();
50 CRM_Contact_BAO_SubscriptionHistory::create($params);
51 }
52 }
53
54 /**
55 * Given the list of params in the params array, fetch the object
56 * and store the values in the values array
57 *
58 * @param array $params
59 * Input parameters to find object.
60 * @param array $values
61 * Output values of the object.
62 *
63 * @return array
64 * (reference) the values that could be potentially assigned to smarty
65 */
66 public static function getValues($params, &$values) {
67 if (empty($params)) {
68 return NULL;
69 }
70 $values['group']['data'] = CRM_Contact_BAO_GroupContact::getContactGroup($params['contact_id'],
71 'Added',
72 3
73 );
74
75 // get the total count of groups
76 $values['group']['totalCount'] = CRM_Contact_BAO_GroupContact::getContactGroup($params['contact_id'],
77 'Added',
78 NULL,
79 TRUE
80 );
81
82 return NULL;
83 }
84
85 /**
86 * Given an array of contact ids, add all the contacts to the group
87 *
88 * @param array $contactIds
89 * The array of contact ids to be added.
90 * @param int $groupId
91 * The id of the group.
92 * @param string $method
93 * @param string $status
94 * @param int $tracking
95 *
96 * @return array
97 * (total, added, notAdded) count of contacts added to group
98 */
99 public static function addContactsToGroup(
100 $contactIds,
101 $groupId,
102 $method = 'Admin',
103 $status = 'Added',
104 $tracking = NULL
105 ) {
106 if (empty($contactIds) || empty($groupId)) {
107 return [];
108 }
109
110 CRM_Utils_Hook::pre('create', 'GroupContact', $groupId, $contactIds);
111
112 $result = self::bulkAddContactsToGroup($contactIds, $groupId, $method, $status, $tracking);
113 CRM_Contact_BAO_GroupContactCache::invalidateGroupContactCache($groupId);
114 CRM_Contact_BAO_Contact_Utils::clearContactCaches();
115
116 CRM_Utils_Hook::post('create', 'GroupContact', $groupId, $contactIds);
117
118 return [count($contactIds), $result['count_added'], $result['count_not_added']];
119 }
120
121 /**
122 * Given an array of contact ids, remove all the contacts from the group
123 *
124 * @param array $contactIds
125 * (reference ) the array of contact ids to be removed.
126 * @param int $groupId
127 * The id of the group.
128 *
129 * @param string $method
130 * @param string $status
131 * @param string $tracking
132 *
133 * @return array
134 * (total, removed, notRemoved) count of contacts removed to group
135 */
136 public static function removeContactsFromGroup(
137 &$contactIds,
138 $groupId,
139 $method = 'Admin',
140 $status = 'Removed',
141 $tracking = NULL
142 ) {
143 if (!is_array($contactIds)) {
144 return [0, 0, 0];
145 }
146 if ($status == 'Removed' || $status == 'Deleted') {
147 $op = 'delete';
148 }
149 else {
150 $op = 'edit';
151 }
152
153 CRM_Utils_Hook::pre($op, 'GroupContact', $groupId, $contactIds);
154
155 $date = date('YmdHis');
156 $numContactsRemoved = 0;
157 $numContactsNotRemoved = 0;
158
159 $group = new CRM_Contact_DAO_Group();
160 $group->id = $groupId;
161 $group->find(TRUE);
162
163 foreach ($contactIds as $contactId) {
164 if ($status == 'Deleted') {
165 $query = "DELETE FROM civicrm_group_contact WHERE contact_id = %1 AND group_id = %2";
166 $dao = CRM_Core_DAO::executeQuery($query, [
167 1 => [$contactId, 'Positive'],
168 2 => [$groupId, 'Positive'],
169 ]);
170 $historyParams = [
171 'group_id' => $groupId,
172 'contact_id' => $contactId,
173 'status' => $status,
174 'method' => $method,
175 'date' => $date,
176 'tracking' => $tracking,
177 ];
178 CRM_Contact_BAO_SubscriptionHistory::create($historyParams);
179 // Removing a row from civicrm_group_contact for a smart group may mean a contact
180 // Is now back in a group based on criteria so we will invalidate the cache if it is there
181 // So that accurate group cache is created next time it is needed.
182 CRM_Contact_BAO_GroupContactCache::invalidateGroupContactCache($groupId);
183 }
184 else {
185 $groupContact = new CRM_Contact_DAO_GroupContact();
186 $groupContact->group_id = $groupId;
187 $groupContact->contact_id = $contactId;
188 // check if the selected contact id already a member, or if this is
189 // an opt-out of a smart group.
190 // if not a member remove to groupContact else keep the count of contacts that are not removed
191 if ($groupContact->find(TRUE) || $group->saved_search_id) {
192 // remove the contact from the group
193 $numContactsRemoved++;
194 }
195 else {
196 $numContactsNotRemoved++;
197 }
198
199 //now we grant the negative membership to contact if not member. CRM-3711
200 $historyParams = [
201 'group_id' => $groupId,
202 'contact_id' => $contactId,
203 'status' => $status,
204 'method' => $method,
205 'date' => $date,
206 'tracking' => $tracking,
207 ];
208 CRM_Contact_BAO_SubscriptionHistory::create($historyParams);
209 $groupContact->status = $status;
210 $groupContact->save();
211 // Remove any rows from the group contact cache so it disappears straight away from smart groups.
212 CRM_Contact_BAO_GroupContactCache::removeContact($contactId, $groupId);
213 }
214 }
215
216 CRM_Contact_BAO_Contact_Utils::clearContactCaches();
217
218 CRM_Utils_Hook::post($op, 'GroupContact', $groupId, $contactIds);
219
220 return [count($contactIds), $numContactsRemoved, $numContactsNotRemoved];
221 }
222
223 /**
224 * Get list of all the groups and groups for a contact.
225 *
226 * @param int $contactId
227 * Contact id.
228 *
229 * @param bool $visibility
230 *
231 *
232 * @return array
233 * this array has key-> group id and value group title
234 */
235 public static function getGroupList($contactId = 0, $visibility = FALSE) {
236 $select = 'SELECT civicrm_group.id, civicrm_group.title ';
237 $from = ' FROM civicrm_group ';
238 $where = " WHERE civicrm_group.is_active = 1 ";
239 if ($contactId) {
240 $from .= ' , civicrm_group_contact ';
241 $where .= " AND civicrm_group.id = civicrm_group_contact.group_id
242 AND civicrm_group_contact.contact_id = " . CRM_Utils_Type::escape($contactId, 'Integer');
243 }
244
245 if ($visibility) {
246 $where .= " AND civicrm_group.visibility != 'User and User Admin Only'";
247 }
248 $groupBy = " GROUP BY civicrm_group.id";
249
250 $orderby = " ORDER BY civicrm_group.name";
251 $sql = $select . $from . $where . $groupBy . $orderby;
252
253 $group = CRM_Core_DAO::executeQuery($sql);
254
255 $values = [];
256 while ($group->fetch()) {
257 $values[$group->id] = $group->title;
258 }
259
260 return $values;
261 }
262
263 /**
264 * Get the list of groups for contact based on status of group membership.
265 *
266 * @param int $contactId
267 * Contact id.
268 * @param string $status
269 * State of membership.
270 * @param int $numGroupContact
271 * Number of groups for a contact that should be shown.
272 * @param bool $count
273 * True if we are interested only in the count.
274 * @param bool $ignorePermission
275 * True if we should ignore permissions for the current user.
276 * useful in profile where permissions are limited for the user. If left
277 * at false only groups viewable by the current user are returned
278 * @param bool $onlyPublicGroups
279 * True if we want to hide system groups.
280 *
281 * @param bool $excludeHidden
282 *
283 * @param int $groupId
284 *
285 * @param bool $includeSmartGroups
286 * Include or Exclude Smart Group(s)
287 * @param bool $public
288 * Are we returning groups for use on a public page.
289 *
290 * @return array|int
291 * the relevant data object values for the contact or the total count when $count is TRUE
292 */
293 public static function getContactGroup(
294 $contactId,
295 $status = NULL,
296 $numGroupContact = NULL,
297 $count = FALSE,
298 $ignorePermission = FALSE,
299 $onlyPublicGroups = FALSE,
300 $excludeHidden = TRUE,
301 $groupId = NULL,
302 $includeSmartGroups = FALSE,
303 $public = FALSE
304 ) {
305 if ($count) {
306 $select = 'SELECT count(DISTINCT civicrm_group_contact.id)';
307 }
308 else {
309 $select = 'SELECT
310 civicrm_group_contact.id as civicrm_group_contact_id,
311 civicrm_group.title as group_title,
312 civicrm_group.frontend_title as group_public_title,
313 civicrm_group.visibility as visibility,
314 civicrm_group_contact.status as status,
315 civicrm_group.id as group_id,
316 civicrm_group.is_hidden as is_hidden,
317 civicrm_subscription_history.date as date,
318 civicrm_subscription_history.method as method';
319 }
320
321 $where = " WHERE contact_a.id = %1 AND civicrm_group.is_active = 1";
322 if (!$includeSmartGroups) {
323 $where .= " AND saved_search_id IS NULL";
324 }
325 if ($excludeHidden) {
326 $where .= " AND civicrm_group.is_hidden = 0 ";
327 }
328 $params = [1 => [$contactId, 'Integer']];
329 if (!empty($status)) {
330 $where .= ' AND civicrm_group_contact.status = %2';
331 $params[2] = [$status, 'String'];
332 }
333 if (!empty($groupId)) {
334 $where .= " AND civicrm_group.id = %3 ";
335 $params[3] = [$groupId, 'Integer'];
336 }
337 $tables = [
338 'civicrm_group_contact' => 1,
339 'civicrm_group' => 1,
340 'civicrm_subscription_history' => 1,
341 ];
342 $whereTables = [];
343 if ($ignorePermission) {
344 $permission = ' ( 1 ) ';
345 }
346 else {
347 $permission = CRM_Core_Permission::getPermissionedStaticGroupClause(CRM_Core_Permission::VIEW, $tables, $whereTables);
348 }
349
350 $from = CRM_Contact_BAO_Query::fromClause($tables);
351
352 $where .= " AND $permission ";
353
354 if ($onlyPublicGroups) {
355 $where .= " AND civicrm_group.visibility != 'User and User Admin Only' ";
356 }
357
358 $order = $limit = '';
359 if (!$count) {
360 $order = ' ORDER BY civicrm_group.title, civicrm_subscription_history.date ASC';
361
362 if ($numGroupContact) {
363 $limit = " LIMIT 0, $numGroupContact";
364 }
365 }
366
367 $sql = $select . $from . $where . $order . $limit;
368
369 if ($count) {
370 $result = CRM_Core_DAO::singleValueQuery($sql, $params);
371 return $result;
372 }
373 else {
374 $dao = CRM_Core_DAO::executeQuery($sql, $params);
375 $values = [];
376 while ($dao->fetch()) {
377 $id = $dao->civicrm_group_contact_id;
378 $values[$id]['id'] = $id;
379 $values[$id]['group_id'] = $dao->group_id;
380 $values[$id]['title'] = ($public && !empty($group->group_public_title) ? $group->group_public_title : $dao->group_title);
381 $values[$id]['visibility'] = $dao->visibility;
382 $values[$id]['is_hidden'] = $dao->is_hidden;
383 switch ($dao->status) {
384 case 'Added':
385 $prefix = 'in_';
386 break;
387
388 case 'Removed':
389 $prefix = 'out_';
390 break;
391
392 default:
393 $prefix = 'pending_';
394 }
395 $values[$id][$prefix . 'date'] = $dao->date;
396 $values[$id][$prefix . 'method'] = $dao->method;
397 if ($status == 'Removed') {
398 $query = "SELECT `date` as `date_added` FROM civicrm_subscription_history WHERE id = (SELECT max(id) FROM civicrm_subscription_history WHERE contact_id = %1 AND status = \"Added\" AND group_id = $dao->group_id )";
399 $dateDAO = CRM_Core_DAO::executeQuery($query, $params);
400 if ($dateDAO->fetch()) {
401 $values[$id]['date_added'] = $dateDAO->date_added;
402 }
403 }
404 }
405 return $values;
406 }
407 }
408
409 /**
410 * Returns membership details of a contact for a group.
411 *
412 * @param int $contactId
413 * Id of the contact.
414 * @param int $groupID
415 * Id of a particular group.
416 * @param string $method
417 * If we want the subscription history details for a specific method.
418 *
419 * @return object
420 * of group contact
421 */
422 public static function getMembershipDetail($contactId, $groupID, $method = 'Email') {
423 $leftJoin = $where = $orderBy = NULL;
424
425 if ($method) {
426 //CRM-13341 add group_id clause
427 $leftJoin = "
428 LEFT JOIN civicrm_subscription_history
429 ON ( civicrm_group_contact.contact_id = civicrm_subscription_history.contact_id
430 AND civicrm_subscription_history.group_id = {$groupID} )";
431 $where = "AND civicrm_subscription_history.method ='Email'";
432 $orderBy = "ORDER BY civicrm_subscription_history.id DESC";
433 }
434 $query = "
435 SELECT *
436 FROM civicrm_group_contact
437 $leftJoin
438 WHERE civicrm_group_contact.contact_id = %1
439 AND civicrm_group_contact.group_id = %2
440 $where
441 $orderBy
442 ";
443
444 $params = [
445 1 => [$contactId, 'Integer'],
446 2 => [$groupID, 'Integer'],
447 ];
448 $dao = CRM_Core_DAO::executeQuery($query, $params);
449 $dao->fetch();
450 return $dao;
451 }
452
453 /**
454 * Method to get Group Id.
455 *
456 * @param int $groupContactID
457 * Id of a particular group.
458 *
459 *
460 * @return int groupID
461 */
462 public static function getGroupId($groupContactID) {
463 $dao = new CRM_Contact_DAO_GroupContact();
464 $dao->id = $groupContactID;
465 $dao->find(TRUE);
466 return $dao->group_id;
467 }
468
469 /**
470 * Deprecated create function.
471 *
472 * @deprecated
473 *
474 * @param array $params
475 *
476 * @return CRM_Contact_DAO_GroupContact
477 */
478 public static function create(array $params) {
479 // @fixme create was only called from CRM_Contact_BAO_Contact::createProfileContact
480 // As of Aug 2020 it's not called from anywhere so we can remove the below code after some time
481
482 CRM_Core_Error::deprecatedFunctionWarning('Use the GroupContact API');
483 return self::add($params);
484 }
485
486 /**
487 * @param int $contactID
488 * @param int $groupID
489 *
490 * @return bool
491 */
492 public static function isContactInGroup($contactID, $groupID) {
493 if (!CRM_Utils_Rule::positiveInteger($contactID) ||
494 !CRM_Utils_Rule::positiveInteger($groupID)
495 ) {
496 return FALSE;
497 }
498
499 $params = [
500 ['group', 'IN', [$groupID], 0, 0],
501 ['contact_id', '=', $contactID, 0, 0],
502 ];
503 [$contacts] = CRM_Contact_BAO_Query::apiQuery($params, ['contact_id']);
504
505 if (!empty($contacts)) {
506 return TRUE;
507 }
508 return FALSE;
509 }
510
511 /**
512 * Function merges the groups from otherContactID to mainContactID.
513 * along with subscription history
514 *
515 * @param int $mainContactId
516 * Contact id of main contact record.
517 * @param int $otherContactId
518 * Contact id of record which is going to merge.
519 *
520 * @see CRM_Dedupe_Merger::cpTables()
521 *
522 * TODO: use the 3rd $sqls param to append sql statements rather than executing them here
523 */
524 public static function mergeGroupContact($mainContactId, $otherContactId) {
525 $params = [
526 1 => [$mainContactId, 'Integer'],
527 2 => [$otherContactId, 'Integer'],
528 ];
529
530 // find all groups that are in otherContactID but not in mainContactID, copy them over
531 $sql = "
532 SELECT cOther.group_id
533 FROM civicrm_group_contact cOther
534 LEFT JOIN civicrm_group_contact cMain ON cOther.group_id = cMain.group_id AND cMain.contact_id = %1
535 WHERE cOther.contact_id = %2
536 AND cMain.contact_id IS NULL
537 ";
538 $dao = CRM_Core_DAO::executeQuery($sql, $params);
539
540 $otherGroupIDs = [];
541 while ($dao->fetch()) {
542 $otherGroupIDs[] = $dao->group_id;
543 }
544
545 if (!empty($otherGroupIDs)) {
546 $otherGroupIDString = implode(',', $otherGroupIDs);
547
548 $sql = "
549 UPDATE civicrm_group_contact
550 SET contact_id = %1
551 WHERE contact_id = %2
552 AND group_id IN ( $otherGroupIDString )
553 ";
554 CRM_Core_DAO::executeQuery($sql, $params);
555
556 $sql = "
557 UPDATE civicrm_subscription_history
558 SET contact_id = %1
559 WHERE contact_id = %2
560 AND group_id IN ( $otherGroupIDString )
561 ";
562 CRM_Core_DAO::executeQuery($sql, $params);
563 }
564
565 $sql = "
566 SELECT cOther.group_id as group_id,
567 cOther.status as group_status
568 FROM civicrm_group_contact cMain
569 INNER JOIN civicrm_group_contact cOther ON cMain.group_id = cOther.group_id
570 WHERE cMain.contact_id = %1
571 AND cOther.contact_id = %2
572 ";
573 $dao = CRM_Core_DAO::executeQuery($sql, $params);
574
575 $groupIDs = [];
576 while ($dao->fetch()) {
577 // only copy it over if it has added status and migrate the history
578 if ($dao->group_status == 'Added') {
579 $groupIDs[] = $dao->group_id;
580 }
581 }
582
583 if (!empty($groupIDs)) {
584 $groupIDString = implode(',', $groupIDs);
585
586 $sql = "
587 UPDATE civicrm_group_contact
588 SET status = 'Added'
589 WHERE contact_id = %1
590 AND group_id IN ( $groupIDString )
591 ";
592 CRM_Core_DAO::executeQuery($sql, $params);
593
594 $sql = "
595 UPDATE civicrm_subscription_history
596 SET contact_id = %1
597 WHERE contact_id = %2
598 AND group_id IN ( $groupIDString )
599 ";
600 CRM_Core_DAO::executeQuery($sql, $params);
601 }
602
603 // delete all the other group contacts
604 $sql = "
605 DELETE
606 FROM civicrm_group_contact
607 WHERE contact_id = %2
608 ";
609 CRM_Core_DAO::executeQuery($sql, $params);
610
611 $sql = "
612 DELETE
613 FROM civicrm_subscription_history
614 WHERE contact_id = %2
615 ";
616 CRM_Core_DAO::executeQuery($sql, $params);
617 }
618
619 /**
620 * Given an array of contact ids, add all the contacts to the group
621 *
622 * @param array $contactIDs
623 * The array of contact ids to be added.
624 * @param int $groupID
625 * The id of the group.
626 * @param string $method
627 * @param string $status
628 * @param string $tracking
629 *
630 * @return array
631 * (total, added, notAdded) count of contacts added to group
632 */
633 public static function bulkAddContactsToGroup(
634 $contactIDs,
635 $groupID,
636 $method = 'Admin',
637 $status = 'Added',
638 $tracking = NULL
639 ) {
640
641 $numContactsAdded = 0;
642 $numContactsNotAdded = 0;
643
644 $contactGroupSQL = "
645 REPLACE INTO civicrm_group_contact ( group_id, contact_id, status )
646 VALUES
647 ";
648 $subscriptioHistorySQL = "
649 INSERT INTO civicrm_subscription_history( group_id, contact_id, date, method, status, tracking )
650 VALUES
651 ";
652
653 $date = date('YmdHis');
654
655 // to avoid long strings, lets do BULK_INSERT_HIGH_COUNT values at a time
656 while (!empty($contactIDs)) {
657 $input = array_splice($contactIDs, 0, CRM_Core_DAO::BULK_INSERT_HIGH_COUNT);
658 $contactStr = implode(',', $input);
659
660 // lets check their current status
661 $sql = "
662 SELECT GROUP_CONCAT(contact_id) as contactStr
663 FROM civicrm_group_contact
664 WHERE group_id = %1
665 AND status = %2
666 AND contact_id IN ( $contactStr )
667 ";
668 $params = [
669 1 => [$groupID, 'Integer'],
670 2 => [$status, 'String'],
671 ];
672
673 $presentIDs = [];
674 $dao = CRM_Core_DAO::executeQuery($sql, $params);
675 if ($dao->fetch()) {
676 $presentIDs = explode(',', $dao->contactStr);
677 $presentIDs = array_flip($presentIDs);
678 }
679
680 $gcValues = $shValues = [];
681 foreach ($input as $cid) {
682 if (isset($presentIDs[$cid])) {
683 $numContactsNotAdded++;
684 continue;
685 }
686
687 $gcValues[] = "( $groupID, $cid, '$status' )";
688 $shValues[] = "( $groupID, $cid, '$date', '$method', '$status', '$tracking' )";
689 $numContactsAdded++;
690 }
691
692 if (!empty($gcValues)) {
693 $cgSQL = $contactGroupSQL . implode(",\n", $gcValues);
694 CRM_Core_DAO::executeQuery($cgSQL);
695
696 $shSQL = $subscriptioHistorySQL . implode(",\n", $shValues);
697 CRM_Core_DAO::executeQuery($shSQL);
698 }
699 }
700
701 return ['count_added' => $numContactsAdded, 'count_not_added' => $numContactsNotAdded];
702 }
703
704 /**
705 * Get options for a given field.
706 * @see CRM_Core_DAO::buildOptions
707 *
708 * @param string $fieldName
709 * @param string $context
710 * @see CRM_Core_DAO::buildOptionsContext
711 * @param array $props
712 * whatever is known about this dao object.
713 *
714 * @return array|bool
715 */
716 public static function buildOptions($fieldName, $context = NULL, $props = []) {
717 $options = CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, [], $context);
718
719 // Sort group list by hierarchy
720 // TODO: This will only work when api.entity is "group_contact". What about others?
721 if (($fieldName == 'group' || $fieldName == 'group_id') && ($context == 'search' || $context == 'create')) {
722 $options = CRM_Contact_BAO_Group::getGroupsHierarchy($options, NULL, '- ', TRUE);
723 }
724
725 return $options;
726 }
727
728 }