Fix hardcoded ID for Head of Household relationship
[civicrm-core.git] / CRM / Contact / BAO / Relationship.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * Class CRM_Contact_BAO_Relationship.
30 */
31 class CRM_Contact_BAO_Relationship extends CRM_Contact_DAO_Relationship {
32
33 /**
34 * Various constants to indicate different type of relationships.
35 *
36 * @var int
37 */
38 const ALL = 0, PAST = 1, DISABLED = 2, CURRENT = 4, INACTIVE = 8;
39
40 /**
41 * Constants for is_permission fields.
42 * Note: the slightly non-obvious ordering is due to history...
43 */
44 const NONE = 0, EDIT = 1, VIEW = 2;
45
46 /**
47 * Create function - use the API instead.
48 *
49 * Note that the previous create function has been renamed 'legacyCreateMultiple'
50 * and this is new in 4.6
51 * All existing calls have been changed to legacyCreateMultiple except the api call - however, it is recommended
52 * that you call that as the end to end testing here is based on the api & refactoring may still be done.
53 *
54 * @param array $params
55 *
56 * @return \CRM_Contact_BAO_Relationship
57 * @throws \CRM_Core_Exception
58 */
59 public static function create(&$params) {
60
61 $extendedParams = self::loadExistingRelationshipDetails($params);
62 // When id is specified we always wan't to update, so we don't need to
63 // check for duplicate relations.
64 if (!isset($params['id']) && self::checkDuplicateRelationship($extendedParams, $extendedParams['contact_id_a'], $extendedParams['contact_id_b'], CRM_Utils_Array::value('id', $extendedParams, 0))) {
65 throw new CRM_Core_Exception('Duplicate Relationship');
66 }
67 $params = $extendedParams;
68 if (self::checkValidRelationship($params, $params, 0)) {
69 throw new CRM_Core_Exception('Invalid Relationship');
70 }
71 $relationship = self::add($params);
72 if (!empty($params['contact_id_a'])) {
73 $ids = [
74 'contactTarget' => $relationship->contact_id_b,
75 'contact' => $params['contact_id_a'],
76 ];
77
78 //CRM-16087 removed additional call to function relatedMemberships which is already called by disableEnableRelationship
79 //resulting in membership being created twice
80 if (array_key_exists('is_active', $params) && empty($params['is_active'])) {
81 $action = CRM_Core_Action::DISABLE;
82 $active = FALSE;
83 }
84 else {
85 $action = CRM_Core_Action::ENABLE;
86 $active = TRUE;
87 }
88 $id = empty($params['id']) ? $relationship->id : $params['id'];
89 self::disableEnableRelationship($id, $action, $params, $ids, $active);
90 }
91
92 if (empty($params['skipRecentView'])) {
93 self::addRecent($params, $relationship);
94 }
95
96 return $relationship;
97 }
98
99 /**
100 * Create multiple relationships for one contact.
101 *
102 * The relationship details are the same for each relationship except the secondary contact
103 * id can be an array.
104 *
105 * @param array $params
106 * Parameters for creating multiple relationships.
107 * The parameters are the same as for relationship create function except that the non-primary
108 * end of the relationship should be an array of one or more contact IDs.
109 * @param string $primaryContactLetter
110 * a or b to denote the primary contact for this action. The secondary may be multiple contacts
111 * and should be an array.
112 *
113 * @return array
114 * @throws \CRM_Core_Exception
115 */
116 public static function createMultiple($params, $primaryContactLetter) {
117 $secondaryContactLetter = ($primaryContactLetter == 'a') ? 'b' : 'a';
118 $secondaryContactIDs = $params['contact_id_' . $secondaryContactLetter];
119 $valid = $invalid = $duplicate = $saved = 0;
120 $relationshipIDs = [];
121 foreach ($secondaryContactIDs as $secondaryContactID) {
122 try {
123 $params['contact_id_' . $secondaryContactLetter] = $secondaryContactID;
124 $relationship = civicrm_api3('relationship', 'create', $params);
125 $relationshipIDs[] = $relationship['id'];
126 $valid++;
127 }
128 catch (CiviCRM_API3_Exception $e) {
129 switch ($e->getMessage()) {
130 case 'Duplicate Relationship':
131 $duplicate++;
132 break;
133
134 case 'Invalid Relationship':
135 $invalid++;
136 break;
137
138 default:
139 throw new CRM_Core_Exception('unknown relationship create error ' . $e->getMessage());
140 }
141 }
142 }
143
144 return [
145 'valid' => $valid,
146 'invalid' => $invalid,
147 'duplicate' => $duplicate,
148 'saved' => $saved,
149 'relationship_ids' => $relationshipIDs,
150 ];
151 }
152
153 /**
154 * Takes an associative array and creates a relationship object.
155 *
156 * @deprecated For single creates use the api instead (it's tested).
157 * For multiple a new variant of this function needs to be written and migrated to as this is a bit
158 * nasty
159 *
160 * @param array $params
161 * (reference ) an assoc array of name/value pairs.
162 * @param array $ids
163 * The array that holds all the db ids.
164 * per http://wiki.civicrm.org/confluence/display/CRM/Database+layer
165 * "we are moving away from the $ids param "
166 *
167 * @return array
168 * @throws \CRM_Core_Exception
169 */
170 public static function legacyCreateMultiple(&$params, $ids = []) {
171 $valid = $invalid = $duplicate = $saved = 0;
172 $relationships = $relationshipIds = [];
173 $relationshipId = CRM_Utils_Array::value('relationship', $ids, CRM_Utils_Array::value('id', $params));
174
175 //CRM-9015 - the hooks are called here & in add (since add doesn't call create)
176 // but in future should be tidied per ticket
177 if (empty($relationshipId)) {
178 $hook = 'create';
179 }
180 else {
181 $hook = 'edit';
182 }
183
184 // @todo pre hook is called from add - remove it from here
185 CRM_Utils_Hook::pre($hook, 'Relationship', $relationshipId, $params);
186
187 if (!$relationshipId) {
188 // creating a new relationship
189 $dataExists = self::dataExists($params);
190 if (!$dataExists) {
191 return [FALSE, TRUE, FALSE, FALSE, NULL];
192 }
193 $relationshipIds = [];
194 foreach ($params['contact_check'] as $key => $value) {
195 // check if the relationship is valid between contacts.
196 // step 1: check if the relationship is valid if not valid skip and keep the count
197 // step 2: check the if two contacts already have a relationship if yes skip and keep the count
198 // step 3: if valid relationship then add the relation and keep the count
199
200 // step 1
201 $contactFields = self::setContactABFromIDs($params, $ids, $key);
202 $errors = self::checkValidRelationship($contactFields, $ids, $key);
203 if ($errors) {
204 $invalid++;
205 continue;
206 }
207
208 //CRM-16978:check duplicate relationship as per case id.
209 if ($caseId = CRM_Utils_Array::value('case_id', $params)) {
210 $contactFields['case_id'] = $caseId;
211 }
212 if (
213 self::checkDuplicateRelationship(
214 $contactFields,
215 CRM_Utils_Array::value('contact', $ids),
216 // step 2
217 $key
218 )
219 ) {
220 $duplicate++;
221 continue;
222 }
223
224 $singleInstanceParams = array_merge($params, $contactFields);
225 $relationship = self::add($singleInstanceParams);
226 $relationshipIds[] = $relationship->id;
227 $relationships[$relationship->id] = $relationship;
228 $valid++;
229 }
230 // editing the relationship
231 }
232 else {
233 // check for duplicate relationship
234 // @todo this code doesn't cope well with updates - causes e-Notices.
235 // API has a lot of code to work around
236 // this but should review this code & remove the extra handling from the api
237 // it seems doubtful any of this is relevant if the contact fields & relationship
238 // type fields are not set
239 if (
240 self::checkDuplicateRelationship(
241 $params,
242 CRM_Utils_Array::value('contact', $ids),
243 $ids['contactTarget'],
244 $relationshipId
245 )
246 ) {
247 $duplicate++;
248 return [$valid, $invalid, $duplicate, $saved, NULL];
249 }
250
251 $validContacts = TRUE;
252 //validate contacts in update mode also.
253 $contactFields = self::setContactABFromIDs($params, $ids, $ids['contactTarget']);
254 if (!empty($ids['contact']) && !empty($ids['contactTarget'])) {
255 if (self::checkValidRelationship($contactFields, $ids, $ids['contactTarget'])) {
256 $validContacts = FALSE;
257 $invalid++;
258 }
259 }
260 if ($validContacts) {
261 // editing an existing relationship
262 $singleInstanceParams = array_merge($params, $contactFields);
263 $relationship = self::add($singleInstanceParams, $ids, $ids['contactTarget']);
264 $relationshipIds[] = $relationship->id;
265 $relationships[$relationship->id] = $relationship;
266 $saved++;
267 }
268 }
269
270 // do not add to recent items for import, CRM-4399
271 if (!(!empty($params['skipRecentView']) || $invalid || $duplicate)) {
272 self::addRecent($params, $relationship);
273 }
274
275 return [$valid, $invalid, $duplicate, $saved, $relationshipIds, $relationships];
276 }
277
278 /**
279 * This is the function that check/add if the relationship created is valid.
280 *
281 * @param array $params
282 * Array of name/value pairs.
283 * @param array $ids
284 * The array that holds all the db ids.
285 * @param int $contactId
286 * This is contact id for adding relationship.
287 *
288 * @return CRM_Contact_BAO_Relationship
289 *
290 * @throws \CiviCRM_API3_Exception
291 */
292 public static function add($params, $ids = [], $contactId = NULL) {
293 $params['id'] = CRM_Utils_Array::value('relationship', $ids, CRM_Utils_Array::value('id', $params));
294
295 $hook = 'create';
296 if ($params['id']) {
297 $hook = 'edit';
298 }
299 CRM_Utils_Hook::pre($hook, 'Relationship', $params['id'], $params);
300
301 $relationshipTypes = CRM_Utils_Array::value('relationship_type_id', $params);
302 // explode the string with _ to get the relationship type id
303 // and to know which contact has to be inserted in
304 // contact_id_a and which one in contact_id_b
305 list($relationshipTypeID) = explode('_', $relationshipTypes);
306
307 $relationship = new CRM_Contact_BAO_Relationship();
308 if (!empty($params['id'])) {
309 $relationship->id = $params['id'];
310 // Only load the relationship if we're missing required params
311 $requiredParams = ['contact_id_a', 'contact_id_b', 'relationship_type_id'];
312 foreach ($requiredParams as $requiredKey) {
313 if (!isset($params[$requiredKey])) {
314 $relationship->find(TRUE);
315 break;
316 }
317 }
318
319 }
320 $relationship->copyValues($params);
321 // @todo we could probably set $params['relationship_type_id'] above but it's unclear
322 // what that would do with the code below this. So for now be conservative and set it manually.
323 if (!empty($relationshipTypeID)) {
324 $relationship->relationship_type_id = $relationshipTypeID;
325 }
326
327 $params['contact_id_a'] = $relationship->contact_id_a;
328 $params['contact_id_b'] = $relationship->contact_id_b;
329
330 // check if the relationship type is Head of Household then update the
331 // household's primary contact with this contact.
332 try {
333 $headOfHouseHoldID = civicrm_api3('RelationshipType', 'getvalue', [
334 'return' => "id",
335 'name_a_b' => "Head of Household for",
336 ]);
337 if ($relationshipTypeID == $headOfHouseHoldID) {
338 CRM_Contact_BAO_Household::updatePrimaryContact($relationship->contact_id_b, $relationship->contact_id_a);
339 }
340 }
341 catch (Exception $e) {
342 // No "Head of Household" relationship found so we skip specific processing
343 }
344
345 if (!empty($params['id']) && self::isCurrentEmployerNeedingToBeCleared($relationship->toArray(), $params['id'], $relationshipTypeID)) {
346 CRM_Contact_BAO_Contact_Utils::clearCurrentEmployer($relationship->contact_id_a);
347 }
348
349 $dateFields = ['end_date', 'start_date'];
350
351 foreach (self::getdefaults() as $defaultField => $defaultValue) {
352 if (isset($params[$defaultField])) {
353 if (in_array($defaultField, $dateFields)) {
354 $relationship->$defaultField = CRM_Utils_Date::format(CRM_Utils_Array::value($defaultField, $params));
355 if (!$relationship->$defaultField) {
356 $relationship->$defaultField = 'NULL';
357 }
358 }
359 else {
360 $relationship->$defaultField = $params[$defaultField];
361 }
362 }
363 elseif (empty($params['id'])) {
364 $relationship->$defaultField = $defaultValue;
365 }
366 }
367
368 $relationship->save();
369 // is_current_employer is an optional parameter that triggers updating the employer_id field to reflect
370 // the relationship being updated. As of writing only truthy versions of the parameter are respected.
371 // https://github.com/civicrm/civicrm-core/pull/13331 attempted to cover both but stalled in QA
372 // so currently we have a cut down version.
373 if (!empty($params['is_current_employer'])) {
374 if (!$relationship->relationship_type_id || !$relationship->contact_id_a || !$relationship->contact_id_b) {
375 $relationship->fetch();
376 }
377 if (self::isRelationshipTypeCurrentEmployer($relationship->relationship_type_id)) {
378 CRM_Contact_BAO_Contact_Utils::setCurrentEmployer([$relationship->contact_id_a => $relationship->contact_id_b]);
379 }
380 }
381 // add custom field values
382 if (!empty($params['custom'])) {
383 CRM_Core_BAO_CustomValueTable::store($params['custom'], 'civicrm_relationship', $relationship->id);
384 }
385
386 CRM_Utils_Hook::post($hook, 'Relationship', $relationship->id, $relationship);
387
388 return $relationship;
389 }
390
391 /**
392 * Add relationship to recent links.
393 *
394 * @param array $params
395 * @param CRM_Contact_DAO_Relationship $relationship
396 */
397 public static function addRecent($params, $relationship) {
398 $url = CRM_Utils_System::url('civicrm/contact/view/rel',
399 "action=view&reset=1&id={$relationship->id}&cid={$relationship->contact_id_a}&context=home"
400 );
401 $session = CRM_Core_Session::singleton();
402 $recentOther = [];
403 if (($session->get('userID') == $relationship->contact_id_a) ||
404 CRM_Contact_BAO_Contact_Permission::allow($relationship->contact_id_a, CRM_Core_Permission::EDIT)
405 ) {
406 $rType = substr(CRM_Utils_Array::value('relationship_type_id', $params), -3);
407 $recentOther = [
408 'editUrl' => CRM_Utils_System::url('civicrm/contact/view/rel',
409 "action=update&reset=1&id={$relationship->id}&cid={$relationship->contact_id_a}&rtype={$rType}&context=home"
410 ),
411 'deleteUrl' => CRM_Utils_System::url('civicrm/contact/view/rel',
412 "action=delete&reset=1&id={$relationship->id}&cid={$relationship->contact_id_a}&rtype={$rType}&context=home"
413 ),
414 ];
415 }
416 $title = CRM_Contact_BAO_Contact::displayName($relationship->contact_id_a) . ' (' . CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType',
417 $relationship->relationship_type_id, 'label_a_b'
418 ) . ' ' . CRM_Contact_BAO_Contact::displayName($relationship->contact_id_b) . ')';
419
420 CRM_Utils_Recent::add($title,
421 $url,
422 $relationship->id,
423 'Relationship',
424 $relationship->contact_id_a,
425 NULL,
426 $recentOther
427 );
428 }
429
430 /**
431 * Load contact ids and relationship type id when doing a create call if not provided.
432 *
433 * There are are various checks done in create which require this information which is optional
434 * when using id.
435 *
436 * @param array $params
437 * Parameters passed to create call.
438 *
439 * @return array
440 * Parameters with missing fields added if required.
441 */
442 public static function loadExistingRelationshipDetails($params) {
443 if (!empty($params['contact_id_a'])
444 && !empty($params['contact_id_b'])
445 && is_numeric($params['relationship_type_id'])) {
446 return $params;
447 }
448 if (empty($params['id'])) {
449 return $params;
450 }
451
452 $fieldsToFill = ['contact_id_a', 'contact_id_b', 'relationship_type_id'];
453 $result = CRM_Core_DAO::executeQuery("SELECT " . implode(',', $fieldsToFill) . " FROM civicrm_relationship WHERE id = %1", [
454 1 => [
455 $params['id'],
456 'Integer',
457 ],
458 ]);
459 while ($result->fetch()) {
460 foreach ($fieldsToFill as $field) {
461 $params[$field] = !empty($params[$field]) ? $params[$field] : $result->$field;
462 }
463 }
464 return $params;
465 }
466
467 /**
468 * Resolve passed in contact IDs to contact_id_a & contact_id_b.
469 *
470 * @param array $params
471 * @param array $ids
472 * @param null $contactID
473 *
474 * @return array
475 * @throws \CRM_Core_Exception
476 */
477 public static function setContactABFromIDs($params, $ids = [], $contactID = NULL) {
478 $returnFields = [];
479
480 // $ids['contact'] is deprecated but comes from legacyCreateMultiple function.
481 if (empty($ids['contact'])) {
482 if (!empty($params['id'])) {
483 return self::loadExistingRelationshipDetails($params);
484 }
485 throw new CRM_Core_Exception('Cannot create relationship, insufficient contact IDs provided');
486 }
487 if (isset($params['relationship_type_id']) && !is_numeric($params['relationship_type_id'])) {
488 $relationshipTypes = CRM_Utils_Array::value('relationship_type_id', $params);
489 list($relationshipTypeID, $first) = explode('_', $relationshipTypes);
490 $returnFields['relationship_type_id'] = $relationshipTypeID;
491
492 foreach (['a', 'b'] as $contactLetter) {
493 if (empty($params['contact_' . $contactLetter])) {
494 if ($first == $contactLetter) {
495 $returnFields['contact_id_' . $contactLetter] = CRM_Utils_Array::value('contact', $ids);
496 }
497 else {
498 $returnFields['contact_id_' . $contactLetter] = $contactID;
499 }
500 }
501 }
502 }
503
504 return $returnFields;
505 }
506
507 /**
508 * Specify defaults for creating a relationship.
509 *
510 * @return array
511 * array of defaults for creating relationship
512 */
513 public static function getdefaults() {
514 return [
515 'is_active' => 1,
516 'is_permission_a_b' => self::NONE,
517 'is_permission_b_a' => self::NONE,
518 'description' => '',
519 'start_date' => 'NULL',
520 'case_id' => NULL,
521 'end_date' => 'NULL',
522 ];
523 }
524
525 /**
526 * Check if there is data to create the object.
527 *
528 * @param array $params
529 * (reference ) an assoc array of name/value pairs.
530 *
531 * @return bool
532 */
533 public static function dataExists(&$params) {
534 // return if no data present
535 if (!is_array(CRM_Utils_Array::value('contact_check', $params))) {
536 return FALSE;
537 }
538 return TRUE;
539 }
540
541 /**
542 * Get get list of relationship type based on the contact type.
543 *
544 * @param int $contactId
545 * This is the contact id of the current contact.
546 * @param null $contactSuffix
547 * @param string $relationshipId
548 * The id of the existing relationship if any.
549 * @param string $contactType
550 * Contact type.
551 * @param bool $all
552 * If true returns relationship types in both the direction.
553 * @param string $column
554 * Name/label that going to retrieve from db.
555 * @param bool $biDirectional
556 * @param array $contactSubType
557 * Includes relationship types between this subtype.
558 * @param bool $onlySubTypeRelationTypes
559 * If set only subtype which is passed by $contactSubType
560 * related relationship types get return
561 *
562 * @return array
563 * array reference of all relationship types with context to current contact.
564 */
565 public static function getContactRelationshipType(
566 $contactId = NULL,
567 $contactSuffix = NULL,
568 $relationshipId = NULL,
569 $contactType = NULL,
570 $all = FALSE,
571 $column = 'label',
572 $biDirectional = TRUE,
573 $contactSubType = NULL,
574 $onlySubTypeRelationTypes = FALSE
575 ) {
576
577 $relationshipType = [];
578 $allRelationshipType = CRM_Core_PseudoConstant::relationshipType($column);
579
580 $otherContactType = NULL;
581 if ($relationshipId) {
582 $relationship = new CRM_Contact_DAO_Relationship();
583 $relationship->id = $relationshipId;
584 if ($relationship->find(TRUE)) {
585 $contact = new CRM_Contact_DAO_Contact();
586 $contact->id = ($relationship->contact_id_a == $contactId) ? $relationship->contact_id_b : $relationship->contact_id_a;
587
588 if ($contact->find(TRUE)) {
589 $otherContactType = $contact->contact_type;
590 //CRM-5125 for contact subtype specific relationshiptypes
591 if ($contact->contact_sub_type) {
592 $otherContactSubType = $contact->contact_sub_type;
593 }
594 }
595 }
596 }
597
598 $contactSubType = (array) $contactSubType;
599 if ($contactId) {
600 $contactType = CRM_Contact_BAO_Contact::getContactType($contactId);
601 $contactSubType = CRM_Contact_BAO_Contact::getContactSubType($contactId);
602 }
603
604 foreach ($allRelationshipType as $key => $value) {
605 // the contact type is required or matches
606 if (((!$value['contact_type_a']) ||
607 $value['contact_type_a'] == $contactType
608 ) &&
609 // the other contact type is required or present or matches
610 ((!$value['contact_type_b']) ||
611 (!$otherContactType) ||
612 $value['contact_type_b'] == $otherContactType
613 ) &&
614 (in_array($value['contact_sub_type_a'], $contactSubType) ||
615 (!$value['contact_sub_type_a'] && !$onlySubTypeRelationTypes)
616 )
617 ) {
618 $relationshipType[$key . '_a_b'] = $value["{$column}_a_b"];
619 }
620
621 if (((!$value['contact_type_b']) ||
622 $value['contact_type_b'] == $contactType
623 ) &&
624 ((!$value['contact_type_a']) ||
625 (!$otherContactType) ||
626 $value['contact_type_a'] == $otherContactType
627 ) &&
628 (in_array($value['contact_sub_type_b'], $contactSubType) ||
629 (!$value['contact_sub_type_b'] && !$onlySubTypeRelationTypes)
630 )
631 ) {
632 $relationshipType[$key . '_b_a'] = $value["{$column}_b_a"];
633 }
634
635 if ($all) {
636 $relationshipType[$key . '_a_b'] = $value["{$column}_a_b"];
637 $relationshipType[$key . '_b_a'] = $value["{$column}_b_a"];
638 }
639 }
640
641 if ($biDirectional) {
642 $relationshipType = self::removeRelationshipTypeDuplicates($relationshipType, $contactSuffix);
643 }
644
645 // sort the relationshipType in ascending order CRM-7736
646 asort($relationshipType);
647 return $relationshipType;
648 }
649
650 /**
651 * Given a list of relationship types, return the list with duplicate types
652 * removed, being careful to retain only the duplicate which matches the given
653 * 'a_b' or 'b_a' suffix.
654 *
655 * @param array $relationshipTypeList A list of relationship types, in the format
656 * returned by self::getContactRelationshipType().
657 * @param string $suffix Either 'a_b' or 'b_a'; defaults to 'a_b'
658 *
659 * @return array The modified value of $relationshipType
660 */
661 public static function removeRelationshipTypeDuplicates($relationshipTypeList, $suffix = NULL) {
662 if (empty($suffix)) {
663 $suffix = 'a_b';
664 }
665
666 // Find those labels which are listed more than once.
667 $duplicateValues = array_diff_assoc($relationshipTypeList, array_unique($relationshipTypeList));
668
669 // For each duplicate label, find its keys, and remove from $relationshipType
670 // the key which does not match $suffix.
671 foreach ($duplicateValues as $value) {
672 $keys = array_keys($relationshipTypeList, $value);
673 foreach ($keys as $key) {
674 if (substr($key, -3) != $suffix) {
675 unset($relationshipTypeList[$key]);
676 }
677 }
678 }
679 return $relationshipTypeList;
680 }
681
682 /**
683 * Delete current employer relationship.
684 *
685 * @param int $id
686 * @param int $action
687 *
688 * @return CRM_Contact_DAO_Relationship
689 */
690 public static function clearCurrentEmployer($id, $action) {
691 $relationship = new CRM_Contact_DAO_Relationship();
692 $relationship->id = $id;
693 $relationship->find(TRUE);
694
695 //to delete relationship between household and individual \
696 //or between individual and organization
697 if (($action & CRM_Core_Action::DISABLE) || ($action & CRM_Core_Action::DELETE)) {
698 $relTypes = CRM_Utils_Array::index(['name_a_b'], CRM_Core_PseudoConstant::relationshipType('name'));
699 if (
700 (isset($relTypes['Employee of']) && $relationship->relationship_type_id == $relTypes['Employee of']['id']) ||
701 (isset($relTypes['Household Member of']) && $relationship->relationship_type_id == $relTypes['Household Member of']['id'])
702 ) {
703 $sharedContact = new CRM_Contact_DAO_Contact();
704 $sharedContact->id = $relationship->contact_id_a;
705 $sharedContact->find(TRUE);
706
707 // CRM-15881 UPDATES
708 // changed FROM "...relationship->relationship_type_id == 4..." TO "...relationship->relationship_type_id == 5..."
709 // As the system should be looking for type "employer of" (id 5) and not "sibling of" (id 4)
710 // As suggested by @davecivicrm, the employee relationship type id is fetched using the CRM_Core_DAO::getFieldValue() class and method, since these ids differ from system to system.
711 $employerRelTypeId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', 'Employee of', 'id', 'name_a_b');
712
713 if ($relationship->relationship_type_id == $employerRelTypeId && $relationship->contact_id_b == $sharedContact->employer_id) {
714 CRM_Contact_BAO_Contact_Utils::clearCurrentEmployer($relationship->contact_id_a);
715 }
716
717 }
718 }
719 return $relationship;
720 }
721
722 /**
723 * Delete the relationship.
724 *
725 * @param int $id
726 * Relationship id.
727 *
728 * @return null
729 * @throws \CRM_Core_Exception
730 */
731 public static function del($id) {
732 // delete from relationship table
733 CRM_Utils_Hook::pre('delete', 'Relationship', $id, CRM_Core_DAO::$_nullArray);
734
735 $relationship = self::clearCurrentEmployer($id, CRM_Core_Action::DELETE);
736 if (CRM_Core_Permission::access('CiviMember')) {
737 // create $params array which isrequired to delete memberships
738 // of the related contacts.
739 $params = [
740 'relationship_type_id' => "{$relationship->relationship_type_id}_a_b",
741 'contact_check' => [$relationship->contact_id_b => 1],
742 ];
743
744 $ids = [];
745 // calling relatedMemberships to delete the memberships of
746 // related contacts.
747 self::relatedMemberships($relationship->contact_id_a,
748 $params,
749 $ids,
750 CRM_Core_Action::DELETE,
751 FALSE
752 );
753 }
754
755 $relationship->delete();
756 CRM_Core_Session::setStatus(ts('Selected relationship has been deleted successfully.'), ts('Record Deleted'), 'success');
757
758 CRM_Utils_Hook::post('delete', 'Relationship', $id, $relationship);
759
760 // delete the recently created Relationship
761 $relationshipRecent = [
762 'id' => $id,
763 'type' => 'Relationship',
764 ];
765 CRM_Utils_Recent::del($relationshipRecent);
766
767 return $relationship;
768 }
769
770 /**
771 * Disable/enable the relationship.
772 *
773 * @param int $id
774 * Relationship id.
775 *
776 * @param int $action
777 * @param array $params
778 * @param array $ids
779 * @param bool $active
780 *
781 * @throws \CRM_Core_Exception
782 */
783 public static function disableEnableRelationship($id, $action, $params = [], $ids = [], $active = FALSE) {
784 $relationship = self::clearCurrentEmployer($id, $action);
785
786 if ($id) {
787 // create $params array which is required to delete memberships
788 // of the related contacts.
789 if (empty($params)) {
790 $params = [
791 'relationship_type_id' => "{$relationship->relationship_type_id}_a_b",
792 'contact_check' => [$relationship->contact_id_b => 1],
793 ];
794 }
795 $contact_id_a = empty($params['contact_id_a']) ? $relationship->contact_id_a : $params['contact_id_a'];
796 // calling relatedMemberships to delete/add the memberships of
797 // related contacts.
798 if ($action & CRM_Core_Action::DISABLE) {
799 CRM_Contact_BAO_Relationship::relatedMemberships($contact_id_a,
800 $params,
801 $ids,
802 CRM_Core_Action::DELETE,
803 $active
804 );
805 }
806 elseif ($action & CRM_Core_Action::ENABLE) {
807 $ids['contact'] = empty($ids['contact']) ? $contact_id_a : $ids['contact'];
808 CRM_Contact_BAO_Relationship::relatedMemberships($contact_id_a,
809 $params,
810 $ids,
811 empty($params['id']) ? CRM_Core_Action::ADD : CRM_Core_Action::UPDATE,
812 $active
813 );
814 }
815 }
816 }
817
818 /**
819 * Delete the object records that are associated with this contact.
820 *
821 * @param int $contactId
822 * Id of the contact to delete.
823 */
824 public static function deleteContact($contactId) {
825 $relationship = new CRM_Contact_DAO_Relationship();
826 $relationship->contact_id_a = $contactId;
827 $relationship->delete();
828
829 $relationship = new CRM_Contact_DAO_Relationship();
830 $relationship->contact_id_b = $contactId;
831 $relationship->delete();
832
833 CRM_Contact_BAO_Household::updatePrimaryContact(NULL, $contactId);
834 }
835
836 /**
837 * Get the other contact in a relationship.
838 *
839 * @param int $id
840 * Relationship id.
841 *
842 * $returns returns the contact ids in the realtionship
843 *
844 * @return \CRM_Contact_DAO_Relationship
845 */
846 public static function getRelationshipByID($id) {
847 $relationship = new CRM_Contact_DAO_Relationship();
848
849 $relationship->id = $id;
850 $relationship->selectAdd();
851 $relationship->selectAdd('contact_id_a, contact_id_b');
852 $relationship->find(TRUE);
853
854 return $relationship;
855 }
856
857 /**
858 * Check if the relationship type selected between two contacts is correct.
859 *
860 * @param int $contact_a
861 * 1st contact id.
862 * @param int $contact_b
863 * 2nd contact id.
864 * @param int $relationshipTypeId
865 * Relationship type id.
866 *
867 * @return bool
868 * true if it is valid relationship else false
869 */
870 public static function checkRelationshipType($contact_a, $contact_b, $relationshipTypeId) {
871 $relationshipType = new CRM_Contact_DAO_RelationshipType();
872 $relationshipType->id = $relationshipTypeId;
873 $relationshipType->selectAdd();
874 $relationshipType->selectAdd('contact_type_a, contact_type_b, contact_sub_type_a, contact_sub_type_b');
875 if ($relationshipType->find(TRUE)) {
876 $contact_type_a = CRM_Contact_BAO_Contact::getContactType($contact_a);
877 $contact_type_b = CRM_Contact_BAO_Contact::getContactType($contact_b);
878
879 $contact_sub_type_a = CRM_Contact_BAO_Contact::getContactSubType($contact_a);
880 $contact_sub_type_b = CRM_Contact_BAO_Contact::getContactSubType($contact_b);
881
882 if (((!$relationshipType->contact_type_a) || ($relationshipType->contact_type_a == $contact_type_a)) &&
883 ((!$relationshipType->contact_type_b) || ($relationshipType->contact_type_b == $contact_type_b)) &&
884 ((!$relationshipType->contact_sub_type_a) || (in_array($relationshipType->contact_sub_type_a,
885 $contact_sub_type_a
886 ))) &&
887 ((!$relationshipType->contact_sub_type_b) || (in_array($relationshipType->contact_sub_type_b,
888 $contact_sub_type_b
889 )))
890 ) {
891 return TRUE;
892 }
893 else {
894 return FALSE;
895 }
896 }
897 return FALSE;
898 }
899
900 /**
901 * This function does the validtion for valid relationship.
902 *
903 * @param array $params
904 * This array contains the values there are subitted by the form.
905 * @param array $ids
906 * The array that holds all the db ids.
907 * @param int $contactId
908 * This is contact id for adding relationship.
909 *
910 * @return string
911 */
912 public static function checkValidRelationship($params, $ids, $contactId) {
913 $errors = '';
914 // function to check if the relationship selected is correct
915 // i.e. employer relationship can exit between Individual and Organization (not between Individual and Individual)
916 if (!CRM_Contact_BAO_Relationship::checkRelationshipType($params['contact_id_a'], $params['contact_id_b'],
917 $params['relationship_type_id'])) {
918 $errors = 'Please select valid relationship between these two contacts.';
919 }
920 return $errors;
921 }
922
923 /**
924 * This function checks for duplicate relationship.
925 *
926 * @param array $params
927 * (reference ) an assoc array of name/value pairs.
928 * @param int $id
929 * This the id of the contact whom we are adding relationship.
930 * @param int $contactId
931 * This is contact id for adding relationship.
932 * @param int $relationshipId
933 * This is relationship id for the contact.
934 *
935 * @return bool
936 * true if record exists else false
937 */
938 public static function checkDuplicateRelationship(&$params, $id, $contactId = 0, $relationshipId = 0) {
939 $relationshipTypeId = CRM_Utils_Array::value('relationship_type_id', $params);
940 list($type) = explode('_', $relationshipTypeId);
941
942 $queryString = "
943 SELECT id
944 FROM civicrm_relationship
945 WHERE relationship_type_id = " . CRM_Utils_Type::escape($type, 'Integer');
946
947 /*
948 * CRM-11792 - date fields from API are in ISO format, but this function
949 * supports date arrays BAO has increasingly standardised to ISO format
950 * so I believe this function should support ISO rather than make API
951 * format it - however, need to support array format for now to avoid breakage
952 * @ time of writing this function is called from Relationship::legacyCreateMultiple (twice)
953 * CRM_BAO_Contact_Utils::clearCurrentEmployer (seemingly without dates)
954 * CRM_Contact_Form_Task_AddToOrganization::postProcess &
955 * CRM_Contact_Form_Task_AddToHousehold::postProcess
956 * (I don't think the last 2 support dates but not sure
957 */
958
959 $dateFields = ['end_date', 'start_date'];
960 foreach ($dateFields as $dateField) {
961 if (array_key_exists($dateField, $params)) {
962 if (empty($params[$dateField]) || $params[$dateField] == 'null') {
963 //this is most likely coming from an api call & probably loaded
964 // from the DB to deal with some of the
965 // other myriad of excessive checks still in place both in
966 // the api & the create functions
967 $queryString .= " AND $dateField IS NULL";
968 continue;
969 }
970 elseif (is_array($params[$dateField])) {
971 $queryString .= " AND $dateField = " .
972 CRM_Utils_Type::escape(CRM_Utils_Date::format($params[$dateField]), 'Date');
973 }
974 else {
975 $queryString .= " AND $dateField = " .
976 CRM_Utils_Type::escape($params[$dateField], 'Date');
977 }
978 }
979 }
980
981 $queryString .=
982 " AND ( ( contact_id_a = " . CRM_Utils_Type::escape($id, 'Integer') .
983 " AND contact_id_b = " . CRM_Utils_Type::escape($contactId, 'Integer') .
984 " ) OR ( contact_id_a = " . CRM_Utils_Type::escape($contactId, 'Integer') .
985 " AND contact_id_b = " . CRM_Utils_Type::escape($id, 'Integer') . " ) ) ";
986
987 //if caseId is provided, include it duplicate checking.
988 if ($caseId = CRM_Utils_Array::value('case_id', $params)) {
989 $queryString .= " AND case_id = " . CRM_Utils_Type::escape($caseId, 'Integer');
990 }
991
992 if ($relationshipId) {
993 $queryString .= " AND id !=" . CRM_Utils_Type::escape($relationshipId, 'Integer');
994 }
995
996 $relationship = new CRM_Contact_BAO_Relationship();
997 $relationship->query($queryString);
998 while ($relationship->fetch()) {
999 // Check whether the custom field values are identical.
1000 $result = self::checkDuplicateCustomFields($params, $relationship->id);
1001 if ($result) {
1002 return TRUE;
1003 }
1004 }
1005 return FALSE;
1006 }
1007
1008 /**
1009 * this function checks whether the values of the custom fields in $params are
1010 * the same as the values of the custom fields of the relation with given
1011 * $relationshipId.
1012 *
1013 * @param array $params (reference) an assoc array of name/value pairs
1014 * @param int $relationshipId ID of an existing duplicate relation
1015 *
1016 * @return boolean true if custom field values are identical
1017 * @access private
1018 * @static
1019 */
1020 private static function checkDuplicateCustomFields(&$params, $relationshipId) {
1021 // Get the custom values of the existing relationship.
1022 $existingValues = CRM_Core_BAO_CustomValueTable::getEntityValues($relationshipId, 'Relationship');
1023 // Create a similar array for the new relationship.
1024 $newValues = [];
1025 if (array_key_exists('custom', $params)) {
1026 // $params['custom'] seems to be an array. Each value is again an array.
1027 // This array contains one value (key -1), and this value seems to be
1028 // an array with the information about the custom value.
1029 foreach ($params['custom'] as $value) {
1030 foreach ($value as $customValue) {
1031 $newValues[$customValue['custom_field_id']] = $customValue['value'];
1032 }
1033 }
1034 }
1035
1036 // Calculate difference between arrays. If the only key-value pairs
1037 // that are in one array but not in the other are empty, the
1038 // custom fields are considered to be equal.
1039 // See https://github.com/civicrm/civicrm-core/pull/6515#issuecomment-137985667
1040 $diff1 = array_diff_assoc($existingValues, $newValues);
1041 $diff2 = array_diff_assoc($newValues, $existingValues);
1042
1043 return !array_filter($diff1) && !array_filter($diff2);
1044 }
1045
1046 /**
1047 * Update the is_active flag in the db.
1048 *
1049 * @param int $id
1050 * Id of the database record.
1051 * @param bool $is_active
1052 * Value we want to set the is_active field.
1053 *
1054 * @return bool
1055 *
1056 * @throws CiviCRM_API3_Exception
1057 */
1058 public static function setIsActive($id, $is_active) {
1059 // as both the create & add functions have a bunch of logic in them that
1060 // doesn't seem to cope with a normal update we will call the api which
1061 // has tested handling for this
1062 // however, a longer term solution would be to simplify the add, create & api functions
1063 // to be more standard. It is debatable @ that point whether it's better to call the BAO
1064 // direct as the api is more tested.
1065 $result = civicrm_api('relationship', 'create', [
1066 'id' => $id,
1067 'is_active' => $is_active,
1068 'version' => 3,
1069 ]);
1070
1071 if (is_array($result) && !empty($result['is_error']) && $result['error_message'] != 'Duplicate Relationship') {
1072 throw new CiviCRM_API3_Exception($result['error_message'], CRM_Utils_Array::value('error_code', $result, 'undefined'), $result);
1073 }
1074
1075 return TRUE;
1076 }
1077
1078 /**
1079 * Fetch a relationship object and store the values in the values array.
1080 *
1081 * @param array $params
1082 * Input parameters to find object.
1083 * @param array $values
1084 * Output values of the object.
1085 *
1086 * @return array
1087 * (reference) the values that could be potentially assigned to smarty
1088 */
1089 public static function &getValues(&$params, &$values) {
1090 if (empty($params)) {
1091 return NULL;
1092 }
1093 $v = [];
1094
1095 // get the specific number of relationship or all relationships.
1096 if (!empty($params['numRelationship'])) {
1097 $v['data'] = &CRM_Contact_BAO_Relationship::getRelationship($params['contact_id'], NULL, $params['numRelationship']);
1098 }
1099 else {
1100 $v['data'] = CRM_Contact_BAO_Relationship::getRelationship($params['contact_id']);
1101 }
1102
1103 // get the total count of relationships
1104 $v['totalCount'] = count($v['data']);
1105
1106 $values['relationship']['data'] = &$v['data'];
1107 $values['relationship']['totalCount'] = &$v['totalCount'];
1108
1109 return $v;
1110 }
1111
1112 /**
1113 * Helper function to form the sql for relationship retrieval.
1114 *
1115 * @param int $contactId
1116 * Contact id.
1117 * @param int $status
1118 * (check const at top of file).
1119 * @param int $numRelationship
1120 * No of relationships to display (limit).
1121 * @param int $count
1122 * Get the no of relationships.
1123 * $param int $relationshipId relationship id
1124 * @param int $relationshipId
1125 * @param string $direction
1126 * The direction we are interested in a_b or b_a.
1127 * @param array $params
1128 * Array of extra values including relationship_type_id per api spec.
1129 *
1130 * @return array
1131 * [select, from, where]
1132 * @throws \Exception
1133 */
1134 public static function makeURLClause($contactId, $status, $numRelationship, $count, $relationshipId, $direction, $params = []) {
1135 $select = $from = $where = '';
1136
1137 $select = '( ';
1138 if ($count) {
1139 if ($direction == 'a_b') {
1140 $select .= ' SELECT count(DISTINCT civicrm_relationship.id) as cnt1, 0 as cnt2 ';
1141 }
1142 else {
1143 $select .= ' SELECT 0 as cnt1, count(DISTINCT civicrm_relationship.id) as cnt2 ';
1144 }
1145 }
1146 else {
1147 $select .= ' SELECT civicrm_relationship.id as civicrm_relationship_id,
1148 civicrm_contact.sort_name as sort_name,
1149 civicrm_contact.display_name as display_name,
1150 civicrm_contact.job_title as job_title,
1151 civicrm_contact.employer_id as employer_id,
1152 civicrm_contact.organization_name as organization_name,
1153 civicrm_address.street_address as street_address,
1154 civicrm_address.city as city,
1155 civicrm_address.postal_code as postal_code,
1156 civicrm_state_province.abbreviation as state,
1157 civicrm_country.name as country,
1158 civicrm_email.email as email,
1159 civicrm_contact.contact_type as contact_type,
1160 civicrm_contact.contact_sub_type as contact_sub_type,
1161 civicrm_phone.phone as phone,
1162 civicrm_contact.id as civicrm_contact_id,
1163 civicrm_relationship.contact_id_b as contact_id_b,
1164 civicrm_relationship.contact_id_a as contact_id_a,
1165 civicrm_relationship_type.id as civicrm_relationship_type_id,
1166 civicrm_relationship.start_date as start_date,
1167 civicrm_relationship.end_date as end_date,
1168 civicrm_relationship.description as description,
1169 civicrm_relationship.is_active as is_active,
1170 civicrm_relationship.is_permission_a_b as is_permission_a_b,
1171 civicrm_relationship.is_permission_b_a as is_permission_b_a,
1172 civicrm_relationship.case_id as case_id';
1173
1174 if ($direction == 'a_b') {
1175 $select .= ', civicrm_relationship_type.label_a_b as label_a_b,
1176 civicrm_relationship_type.label_b_a as relation ';
1177 }
1178 else {
1179 $select .= ', civicrm_relationship_type.label_a_b as label_a_b,
1180 civicrm_relationship_type.label_a_b as relation ';
1181 }
1182 }
1183
1184 $from = "
1185 FROM civicrm_relationship
1186 INNER JOIN civicrm_relationship_type ON ( civicrm_relationship.relationship_type_id = civicrm_relationship_type.id )
1187 INNER JOIN civicrm_contact ";
1188 if ($direction == 'a_b') {
1189 $from .= 'ON ( civicrm_contact.id = civicrm_relationship.contact_id_a ) ';
1190 }
1191 else {
1192 $from .= 'ON ( civicrm_contact.id = civicrm_relationship.contact_id_b ) ';
1193 }
1194
1195 if (!$count) {
1196 $from .= "
1197 LEFT JOIN civicrm_address ON (civicrm_address.contact_id = civicrm_contact.id AND civicrm_address.is_primary = 1)
1198 LEFT JOIN civicrm_phone ON (civicrm_phone.contact_id = civicrm_contact.id AND civicrm_phone.is_primary = 1)
1199 LEFT JOIN civicrm_email ON (civicrm_email.contact_id = civicrm_contact.id AND civicrm_email.is_primary = 1)
1200 LEFT JOIN civicrm_state_province ON (civicrm_address.state_province_id = civicrm_state_province.id)
1201 LEFT JOIN civicrm_country ON (civicrm_address.country_id = civicrm_country.id)
1202 ";
1203 }
1204
1205 $where = 'WHERE ( 1 )';
1206 if ($contactId) {
1207 if ($direction == 'a_b') {
1208 $where .= ' AND civicrm_relationship.contact_id_b = ' . CRM_Utils_Type::escape($contactId, 'Positive');
1209 }
1210 else {
1211 $where .= ' AND civicrm_relationship.contact_id_a = ' . CRM_Utils_Type::escape($contactId, 'Positive') . '
1212 AND civicrm_relationship.contact_id_a != civicrm_relationship.contact_id_b ';
1213 }
1214 }
1215 if ($relationshipId) {
1216 $where .= ' AND civicrm_relationship.id = ' . CRM_Utils_Type::escape($relationshipId, 'Positive');
1217 }
1218
1219 $date = date('Y-m-d');
1220 if ($status == self::PAST) {
1221 //this case for showing past relationship
1222 $where .= ' AND civicrm_relationship.is_active = 1 ';
1223 $where .= " AND civicrm_relationship.end_date < '" . $date . "'";
1224 }
1225 elseif ($status == self::DISABLED) {
1226 // this case for showing disabled relationship
1227 $where .= ' AND civicrm_relationship.is_active = 0 ';
1228 }
1229 elseif ($status == self::CURRENT) {
1230 //this case for showing current relationship
1231 $where .= ' AND civicrm_relationship.is_active = 1 ';
1232 $where .= " AND (civicrm_relationship.end_date >= '" . $date . "' OR civicrm_relationship.end_date IS NULL) ";
1233 }
1234 elseif ($status == self::INACTIVE) {
1235 //this case for showing inactive relationships
1236 $where .= " AND (civicrm_relationship.end_date < '" . $date . "'";
1237 $where .= ' OR civicrm_relationship.is_active = 0 )';
1238 }
1239
1240 // CRM-6181
1241 $where .= ' AND civicrm_contact.is_deleted = 0';
1242 if (!empty($params['membership_type_id']) && empty($params['relationship_type_id'])) {
1243 $where .= self::membershipTypeToRelationshipTypes($params, $direction);
1244 }
1245 if (!empty($params['relationship_type_id'])) {
1246 if (is_array($params['relationship_type_id'])) {
1247 $where .= " AND " . CRM_Core_DAO::createSQLFilter('relationship_type_id', $params['relationship_type_id'], 'Integer');
1248 }
1249 else {
1250 $where .= ' AND relationship_type_id = ' . CRM_Utils_Type::escape($params['relationship_type_id'], 'Positive');
1251 }
1252 }
1253 if ($direction == 'a_b') {
1254 $where .= ' ) UNION ';
1255 }
1256 else {
1257 $where .= ' ) ';
1258 }
1259
1260 return [$select, $from, $where];
1261 }
1262
1263 /**
1264 * Get a list of relationships.
1265 *
1266 * @param int $contactId
1267 * Contact id.
1268 * @param int $status
1269 * 1: Past 2: Disabled 3: Current.
1270 * @param int $numRelationship
1271 * No of relationships to display (limit).
1272 * @param int $count
1273 * Get the no of relationships.
1274 * @param int $relationshipId
1275 * @param array $links
1276 * the list of links to display
1277 * @param int $permissionMask
1278 * the permission mask to be applied for the actions
1279 * @param bool $permissionedContact
1280 * to return only permissioned Contact
1281 * @param array $params
1282 * @param bool $includeTotalCount
1283 * Should we return a count of total accessable relationships
1284 *
1285 * @return array|int
1286 * relationship records
1287 * @throws \Exception
1288 */
1289 public static function getRelationship(
1290 $contactId = NULL,
1291 $status = 0, $numRelationship = 0,
1292 $count = 0, $relationshipId = 0,
1293 $links = NULL, $permissionMask = NULL,
1294 $permissionedContact = FALSE,
1295 $params = [], $includeTotalCount = FALSE
1296 ) {
1297 $values = [];
1298 if (!$contactId && !$relationshipId) {
1299 return $values;
1300 }
1301
1302 list($select1, $from1, $where1) = self::makeURLClause($contactId, $status, $numRelationship,
1303 $count, $relationshipId, 'a_b', $params
1304 );
1305 list($select2, $from2, $where2) = self::makeURLClause($contactId, $status, $numRelationship,
1306 $count, $relationshipId, 'b_a', $params
1307 );
1308
1309 $order = $limit = '';
1310 if (!$count) {
1311 if (empty($params['sort'])) {
1312 $order = ' ORDER BY civicrm_relationship_type_id, sort_name ';
1313 }
1314 else {
1315 $order = " ORDER BY {$params['sort']} ";
1316 }
1317
1318 $offset = 0;
1319 if (!empty($params['offset']) && $params['offset'] > 0) {
1320 $offset = $params['offset'];
1321 }
1322
1323 if ($numRelationship) {
1324 $limit = " LIMIT {$offset}, $numRelationship";
1325 }
1326 }
1327
1328 // building the query string
1329 $queryString = $select1 . $from1 . $where1 . $select2 . $from2 . $where2;
1330
1331 $relationship = new CRM_Contact_DAO_Relationship();
1332
1333 $relationship->query($queryString . $order . $limit);
1334 $row = [];
1335 if ($count) {
1336 $relationshipCount = 0;
1337 while ($relationship->fetch()) {
1338 $relationshipCount += $relationship->cnt1 + $relationship->cnt2;
1339 }
1340 return $relationshipCount;
1341 }
1342 else {
1343
1344 if ($includeTotalCount) {
1345 $values['total_relationships'] = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM ({$queryString}) AS r");
1346 }
1347
1348 $mask = NULL;
1349 if ($status != self::INACTIVE) {
1350 if ($links) {
1351 $mask = array_sum(array_keys($links));
1352 if ($mask & CRM_Core_Action::DISABLE) {
1353 $mask -= CRM_Core_Action::DISABLE;
1354 }
1355 if ($mask & CRM_Core_Action::ENABLE) {
1356 $mask -= CRM_Core_Action::ENABLE;
1357 }
1358
1359 if ($status == self::CURRENT) {
1360 $mask |= CRM_Core_Action::DISABLE;
1361 }
1362 elseif ($status == self::DISABLED) {
1363 $mask |= CRM_Core_Action::ENABLE;
1364 }
1365 }
1366 // temporary hold the value of $mask.
1367 $tempMask = $mask;
1368 }
1369
1370 while ($relationship->fetch()) {
1371 $rid = $relationship->civicrm_relationship_id;
1372 $cid = $relationship->civicrm_contact_id;
1373
1374 if ($permissionedContact &&
1375 (!CRM_Contact_BAO_Contact_Permission::allow($cid))
1376 ) {
1377 continue;
1378 }
1379 if ($status != self::INACTIVE && $links) {
1380 // assign the original value to $mask
1381 $mask = $tempMask;
1382 // display action links if $cid has edit permission for the relationship.
1383 if (!($permissionMask & CRM_Core_Permission::EDIT) && CRM_Contact_BAO_Contact_Permission::allow($cid, CRM_Core_Permission::EDIT)) {
1384 $permissions[] = CRM_Core_Permission::EDIT;
1385 $permissions[] = CRM_Core_Permission::DELETE;
1386 $permissionMask = CRM_Core_Action::mask($permissions);
1387 }
1388 $mask = $mask & $permissionMask;
1389 }
1390 $values[$rid]['id'] = $rid;
1391 $values[$rid]['cid'] = $cid;
1392 $values[$rid]['contact_id_a'] = $relationship->contact_id_a;
1393 $values[$rid]['contact_id_b'] = $relationship->contact_id_b;
1394 $values[$rid]['contact_type'] = $relationship->contact_type;
1395 $values[$rid]['contact_sub_type'] = $relationship->contact_sub_type;
1396 $values[$rid]['relationship_type_id'] = $relationship->civicrm_relationship_type_id;
1397 $values[$rid]['relation'] = $relationship->relation;
1398 $values[$rid]['name'] = $relationship->sort_name;
1399 $values[$rid]['display_name'] = $relationship->display_name;
1400 $values[$rid]['job_title'] = $relationship->job_title;
1401 $values[$rid]['email'] = $relationship->email;
1402 $values[$rid]['phone'] = $relationship->phone;
1403 $values[$rid]['employer_id'] = $relationship->employer_id;
1404 $values[$rid]['organization_name'] = $relationship->organization_name;
1405 $values[$rid]['country'] = $relationship->country;
1406 $values[$rid]['city'] = $relationship->city;
1407 $values[$rid]['state'] = $relationship->state;
1408 $values[$rid]['start_date'] = $relationship->start_date;
1409 $values[$rid]['end_date'] = $relationship->end_date;
1410 $values[$rid]['description'] = $relationship->description;
1411 $values[$rid]['is_active'] = $relationship->is_active;
1412 $values[$rid]['is_permission_a_b'] = $relationship->is_permission_a_b;
1413 $values[$rid]['is_permission_b_a'] = $relationship->is_permission_b_a;
1414 $values[$rid]['case_id'] = $relationship->case_id;
1415
1416 if ($status) {
1417 $values[$rid]['status'] = $status;
1418 }
1419
1420 $values[$rid]['civicrm_relationship_type_id'] = $relationship->civicrm_relationship_type_id;
1421
1422 if ($relationship->contact_id_a == $contactId) {
1423 $values[$rid]['rtype'] = 'a_b';
1424 }
1425 else {
1426 $values[$rid]['rtype'] = 'b_a';
1427 }
1428
1429 if ($links) {
1430 $replace = [
1431 'id' => $rid,
1432 'rtype' => $values[$rid]['rtype'],
1433 'cid' => $contactId,
1434 'cbid' => $values[$rid]['cid'],
1435 'caseid' => $values[$rid]['case_id'],
1436 'clientid' => $contactId,
1437 ];
1438
1439 if ($status == self::INACTIVE) {
1440 // setting links for inactive relationships
1441 $mask = array_sum(array_keys($links));
1442 if (!$values[$rid]['is_active']) {
1443 $mask -= CRM_Core_Action::DISABLE;
1444 }
1445 else {
1446 $mask -= CRM_Core_Action::ENABLE;
1447 $mask -= CRM_Core_Action::DISABLE;
1448 }
1449 $mask = $mask & $permissionMask;
1450 }
1451
1452 // Give access to manage case link by copying to MAX_ACTION index temporarily, depending on case permission of user.
1453 if ($values[$rid]['case_id']) {
1454 // Borrowed logic from CRM_Case_Page_Tab
1455 $hasCaseAccess = FALSE;
1456 if (CRM_Core_Permission::check('access all cases and activities')) {
1457 $hasCaseAccess = TRUE;
1458 }
1459 else {
1460 $userCases = CRM_Case_BAO_Case::getCases(FALSE);
1461 if (array_key_exists($values[$rid]['case_id'], $userCases)) {
1462 $hasCaseAccess = TRUE;
1463 }
1464 }
1465
1466 if ($hasCaseAccess) {
1467 // give access by copying to MAX_ACTION temporarily, otherwise leave at NONE which won't display
1468 $links[CRM_Core_Action::MAX_ACTION] = $links[CRM_Core_Action::NONE];
1469 $links[CRM_Core_Action::MAX_ACTION]['name'] = ts('Manage Case #%1', [1 => $values[$rid]['case_id']]);
1470 $links[CRM_Core_Action::MAX_ACTION]['class'] = 'no-popup';
1471
1472 // Also make sure we have the right client cid since can get here from multiple relationship tabs.
1473 if ($values[$rid]['rtype'] == 'b_a') {
1474 $replace['clientid'] = $values[$rid]['cid'];
1475 }
1476 $values[$rid]['case'] = '<a href="' . CRM_Utils_System::url('civicrm/case/ajax/details', sprintf('caseId=%d&cid=%d&snippet=4', $values[$rid]['case_id'], $values[$rid]['cid'])) . '" class="action-item crm-hover-button crm-summary-link"><i class="crm-i fa-folder-open-o"></i></a>';
1477 }
1478 }
1479
1480 $values[$rid]['action'] = CRM_Core_Action::formLink(
1481 $links,
1482 $mask,
1483 $replace,
1484 ts('more'),
1485 FALSE,
1486 'relationship.selector.row',
1487 'Relationship',
1488 $rid);
1489 unset($links[CRM_Core_Action::MAX_ACTION]);
1490 }
1491 }
1492
1493 return $values;
1494 }
1495 }
1496
1497 /**
1498 * Get get list of relationship type based on the target contact type.
1499 *
1500 * @param string $targetContactType
1501 * It's valid contact tpye(may be Individual , Organization , Household).
1502 *
1503 * @return array
1504 * array reference of all relationship types with context to current contact type .
1505 */
1506 public static function getRelationType($targetContactType) {
1507 $relationshipType = [];
1508 $allRelationshipType = CRM_Core_PseudoConstant::relationshipType();
1509
1510 foreach ($allRelationshipType as $key => $type) {
1511 if ($type['contact_type_b'] == $targetContactType || empty($type['contact_type_b'])) {
1512 $relationshipType[$key . '_a_b'] = $type['label_a_b'];
1513 }
1514 }
1515
1516 return $relationshipType;
1517 }
1518
1519 /**
1520 * Create / update / delete membership for related contacts.
1521 *
1522 * This function will create/update/delete membership for related
1523 * contact based on 1) contact have active membership 2) that
1524 * membership is is extedned by the same relationship type to that
1525 * of the existing relationship.
1526 *
1527 * @param int $contactId
1528 * contact id.
1529 * @param array $params
1530 * array of values submitted by POST.
1531 * @param array $ids
1532 * array of ids.
1533 * @param \const|int $action which action called this function
1534 *
1535 * @param bool $active
1536 *
1537 * @throws \CRM_Core_Exception
1538 */
1539 public static function relatedMemberships($contactId, &$params, $ids, $action = CRM_Core_Action::ADD, $active = TRUE) {
1540 // Check the end date and set the status of the relationship
1541 // accordingly.
1542 $status = self::CURRENT;
1543 $targetContact = $targetContact = CRM_Utils_Array::value('contact_check', $params, []);
1544 $today = date('Ymd');
1545
1546 // If a relationship hasn't yet started, just return for now
1547 // TODO: handle edge-case of updating start_date of an existing relationship
1548 if (!empty($params['start_date'])) {
1549 $startDate = substr(CRM_Utils_Date::format($params['start_date']), 0, 8);
1550 if ($today < $startDate) {
1551 return;
1552 }
1553 }
1554
1555 if (!empty($params['end_date'])) {
1556 $endDate = substr(CRM_Utils_Date::format($params['end_date']), 0, 8);
1557 if ($today > $endDate) {
1558 $status = self::PAST;
1559 }
1560 }
1561
1562 if (($action & CRM_Core_Action::ADD) && ($status & self::PAST)) {
1563 // If relationship is PAST and action is ADD, do nothing.
1564 return;
1565 }
1566
1567 $rel = explode('_', $params['relationship_type_id']);
1568
1569 $relTypeId = $rel[0];
1570 if (!empty($rel[1])) {
1571 $relDirection = "_{$rel[1]}_{$rel[2]}";
1572 }
1573 else {
1574 // this call is coming from somewhere where the direction was resolved early on (e.g an api call)
1575 // so we can assume _a_b
1576 $relDirection = "_a_b";
1577 $targetContact = [$params['contact_id_b'] => 1];
1578 }
1579
1580 if (($action & CRM_Core_Action::ADD) ||
1581 ($action & CRM_Core_Action::DELETE)
1582 ) {
1583 $contact = $contactId;
1584 }
1585 elseif ($action & CRM_Core_Action::UPDATE) {
1586 $contact = $ids['contact'];
1587 $targetContact = [$ids['contactTarget'] => 1];
1588 }
1589
1590 // Build the 'values' array for
1591 // 1. ContactA
1592 // 2. ContactB
1593 // This will allow us to check if either of the contacts in
1594 // relationship have active memberships.
1595
1596 $values = [];
1597
1598 // 1. ContactA
1599 $values[$contact] = [
1600 'relatedContacts' => $targetContact,
1601 'relationshipTypeId' => $relTypeId,
1602 'relationshipTypeDirection' => $relDirection,
1603 ];
1604 // 2. ContactB
1605 if (!empty($targetContact)) {
1606 foreach ($targetContact as $cid => $donCare) {
1607 $values[$cid] = [
1608 'relatedContacts' => [$contact => 1],
1609 'relationshipTypeId' => $relTypeId,
1610 ];
1611
1612 $relTypeParams = ['id' => $relTypeId];
1613 $relTypeValues = [];
1614 CRM_Contact_BAO_RelationshipType::retrieve($relTypeParams, $relTypeValues);
1615
1616 if (CRM_Utils_Array::value('name_a_b', $relTypeValues) == CRM_Utils_Array::value('name_b_a', $relTypeValues)) {
1617 $values[$cid]['relationshipTypeDirection'] = '_a_b';
1618 }
1619 else {
1620 $values[$cid]['relationshipTypeDirection'] = ($relDirection == '_a_b') ? '_b_a' : '_a_b';
1621 }
1622 }
1623 }
1624
1625 // CRM-15829 UPDATES
1626 // If we're looking for active memberships we must consider pending (id: 5) ones too.
1627 // Hence we can't just call CRM_Member_BAO_Membership::getValues below with the active flag, is it would completely miss pending relatioships.
1628 // As suggested by @davecivicrm, the pending status id is fetched using the CRM_Member_PseudoConstant::membershipStatus() class and method, since these ids differ from system to system.
1629 $pendingStatusId = array_search('Pending', CRM_Member_PseudoConstant::membershipStatus());
1630
1631 $query = 'SELECT * FROM `civicrm_membership_status`';
1632 if ($active) {
1633 $query .= ' WHERE `is_current_member` = 1 OR `id` = %1 ';
1634 }
1635
1636 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$pendingStatusId, 'Integer']]);
1637
1638 while ($dao->fetch()) {
1639 $membershipStatusRecordIds[$dao->id] = $dao->id;
1640 }
1641
1642 // Now get the active memberships for all the contacts.
1643 // If contact have any valid membership(s), then add it to
1644 // 'values' array.
1645 foreach ($values as $cid => $subValues) {
1646 $memParams = ['contact_id' => $cid];
1647 $memberships = [];
1648
1649 // CRM-15829 UPDATES
1650 // Since we want PENDING memberships as well, the $active flag needs to be set to false so that this will return all memberships and we can then filter the memberships based on the status IDs recieved above.
1651 CRM_Member_BAO_Membership::getValues($memParams, $memberships, FALSE, TRUE);
1652
1653 // CRM-15829 UPDATES
1654 // filter out the memberships returned by CRM_Member_BAO_Membership::getValues based on the status IDs fetched on line ~1462
1655 foreach ($memberships as $key => $membership) {
1656
1657 if (!isset($memberships[$key]['status_id'])) {
1658 continue;
1659 }
1660
1661 $membershipStatusId = $memberships[$key]['status_id'];
1662 if (!isset($membershipStatusRecordIds[$membershipStatusId])) {
1663 unset($memberships[$key]);
1664 }
1665 }
1666
1667 if (empty($memberships)) {
1668 continue;
1669 }
1670
1671 //get ownerMembershipIds for related Membership
1672 //this is to handle memberships being deleted and recreated
1673 if (!empty($memberships['owner_membership_ids'])) {
1674 $ownerMemIds[$cid] = $memberships['owner_membership_ids'];
1675 unset($memberships['owner_membership_ids']);
1676 }
1677
1678 $values[$cid]['memberships'] = $memberships;
1679 }
1680 $deceasedStatusId = array_search('Deceased', CRM_Member_PseudoConstant::membershipStatus());
1681
1682 // done with 'values' array.
1683 // Finally add / edit / delete memberships for the related contacts
1684
1685 foreach ($values as $cid => $details) {
1686 if (!array_key_exists('memberships', $details)) {
1687 continue;
1688 }
1689
1690 $relatedContacts = array_keys(CRM_Utils_Array::value('relatedContacts', $details, []));
1691 $mainRelatedContactId = reset($relatedContacts);
1692
1693 foreach ($details['memberships'] as $membershipId => $membershipValues) {
1694 $relTypeIds = [];
1695 if ($action & CRM_Core_Action::DELETE) {
1696 // @todo don't return relTypeId here - but it seems to be used later in a cryptic way (hint cryptic is not a complement).
1697 list($relTypeId, $isDeletable) = self::isInheritedMembershipInvalidated($membershipValues, $values, $cid, $mainRelatedContactId);
1698 if ($isDeletable) {
1699 CRM_Member_BAO_Membership::deleteRelatedMemberships($membershipValues['owner_membership_id'], $membershipValues['membership_contact_id']);
1700 }
1701 continue;
1702 }
1703 if (($action & CRM_Core_Action::UPDATE) &&
1704 ($status & self::PAST) &&
1705 ($membershipValues['owner_membership_id'])
1706 ) {
1707 // If relationship is PAST and action is UPDATE
1708 // then delete the RELATED membership
1709 CRM_Member_BAO_Membership::deleteRelatedMemberships($membershipValues['owner_membership_id'],
1710 $membershipValues['membership_contact_id']
1711 );
1712 continue;
1713 }
1714
1715 // add / edit the memberships for related
1716 // contacts.
1717
1718 // Get the Membership Type Details.
1719 $membershipType = CRM_Member_BAO_MembershipType::getMembershipTypeDetails($membershipValues['membership_type_id']);
1720 // Check if contact's relationship type exists in membership type
1721 $relTypeDirs = [];
1722 if (!empty($membershipType['relationship_type_id'])) {
1723 $relTypeIds = explode(CRM_Core_DAO::VALUE_SEPARATOR, $membershipType['relationship_type_id']);
1724 }
1725 if (!empty($membershipType['relationship_direction'])) {
1726 $relDirections = explode(CRM_Core_DAO::VALUE_SEPARATOR, $membershipType['relationship_direction']);
1727 }
1728 foreach ($relTypeIds as $key => $value) {
1729 $relTypeDirs[] = $value . '_' . $relDirections[$key];
1730 }
1731 $relTypeDir = $details['relationshipTypeId'] . $details['relationshipTypeDirection'];
1732 if (in_array($relTypeDir, $relTypeDirs)) {
1733 // Check if relationship being created/updated is
1734 // similar to that of membership type's
1735 // relationship.
1736
1737 $membershipValues['owner_membership_id'] = $membershipId;
1738 unset($membershipValues['id']);
1739 unset($membershipValues['membership_contact_id']);
1740 unset($membershipValues['contact_id']);
1741 unset($membershipValues['membership_id']);
1742 foreach ($details['relatedContacts'] as $relatedContactId => $donCare) {
1743 $membershipValues['contact_id'] = $relatedContactId;
1744 if ($deceasedStatusId &&
1745 CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $relatedContactId, 'is_deceased')
1746 ) {
1747 $membershipValues['status_id'] = $deceasedStatusId;
1748 $membershipValues['skipStatusCal'] = TRUE;
1749 }
1750 foreach (['join_date', 'start_date', 'end_date'] as $dateField) {
1751 if (!empty($membershipValues[$dateField])) {
1752 $membershipValues[$dateField] = CRM_Utils_Date::processDate($membershipValues[$dateField]);
1753 }
1754 }
1755
1756 if ($action & CRM_Core_Action::UPDATE) {
1757 //if updated relationship is already related to contact don't delete existing inherited membership
1758 if (in_array($relTypeId, $relTypeIds
1759 ) && !empty($values[$relatedContactId]['memberships']) && !empty($ownerMemIds
1760 ) && in_array($membershipValues['owner_membership_id'], $ownerMemIds[$relatedContactId])) {
1761 continue;
1762 }
1763
1764 //delete the membership record for related
1765 //contact before creating new membership record.
1766 CRM_Member_BAO_Membership::deleteRelatedMemberships($membershipId, $relatedContactId);
1767 }
1768 //skip status calculation for pay later memberships.
1769 if (!empty($membershipValues['status_id']) && $membershipValues['status_id'] == $pendingStatusId) {
1770 $membershipValues['skipStatusCal'] = TRUE;
1771 }
1772
1773 // check whether we have some related memberships still available
1774 $query = "
1775 SELECT count(*)
1776 FROM civicrm_membership
1777 LEFT JOIN civicrm_membership_status ON (civicrm_membership_status.id = civicrm_membership.status_id)
1778 WHERE membership_type_id = {$membershipValues['membership_type_id']} AND owner_membership_id = {$membershipValues['owner_membership_id']}
1779 AND is_current_member = 1";
1780 $result = CRM_Core_DAO::singleValueQuery($query);
1781 if ($result < CRM_Utils_Array::value('max_related', $membershipValues, PHP_INT_MAX)) {
1782 CRM_Member_BAO_Membership::create($membershipValues);
1783 }
1784 }
1785 }
1786 elseif ($action & CRM_Core_Action::UPDATE) {
1787 // if action is update and updated relationship do
1788 // not match with the existing
1789 // membership=>relationship then we need to
1790 // change the status of the membership record to expired for
1791 // previous relationship -- CRM-12078.
1792 // CRM-16087 we need to pass ownerMembershipId to isRelatedMembershipExpired function
1793 if (empty($params['relationship_ids']) && !empty($params['id'])) {
1794 $relIds = [$params['id']];
1795 }
1796 else {
1797 $relIds = CRM_Utils_Array::value('relationship_ids', $params);
1798 }
1799 if (self::isRelatedMembershipExpired($relTypeIds, $contactId, $mainRelatedContactId, $relTypeId,
1800 $relIds) && !empty($membershipValues['owner_membership_id']
1801 ) && !empty($values[$mainRelatedContactId]['memberships'][$membershipValues['owner_membership_id']])) {
1802 $membershipValues['status_id'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipStatus', 'Expired', 'id', 'label');
1803 $type = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipType', $membershipValues['membership_type_id'], 'name', 'id');
1804 CRM_Member_BAO_Membership::add($membershipValues);
1805 CRM_Core_Session::setStatus(ts("Inherited membership %1 status was changed to Expired due to the change in relationship type.", [1 => $type]), ts('Record Updated'), 'alert');
1806 }
1807 }
1808 }
1809 }
1810 }
1811
1812 /**
1813 * Helper function to check whether the membership is expired or not.
1814 *
1815 * Function takes a list of related membership types and if it is not also passed a
1816 * relationship ID of that types evaluates whether the membership status should be changed to expired.
1817 *
1818 * @param array $membershipTypeRelationshipTypeIDs
1819 * Relation type IDs related to the given membership type.
1820 * @param int $contactId
1821 * @param int $mainRelatedContactId
1822 * @param int $relTypeId
1823 * @param array $relIds
1824 *
1825 * @return bool
1826 */
1827 public static function isRelatedMembershipExpired($membershipTypeRelationshipTypeIDs, $contactId, $mainRelatedContactId, $relTypeId, $relIds) {
1828 if (empty($membershipTypeRelationshipTypeIDs) || in_array($relTypeId, $membershipTypeRelationshipTypeIDs)) {
1829 return FALSE;
1830 }
1831
1832 if (empty($relIds)) {
1833 return FALSE;
1834 }
1835
1836 $relParamas = [
1837 1 => [$contactId, 'Integer'],
1838 2 => [$mainRelatedContactId, 'Integer'],
1839 ];
1840
1841 if ($contactId == $mainRelatedContactId) {
1842 $recordsFound = (int) CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM civicrm_relationship WHERE relationship_type_id IN ( " . implode(',', $membershipTypeRelationshipTypeIDs) . " ) AND
1843 contact_id_a IN ( %1 ) OR contact_id_b IN ( %1 ) AND id IN (" . implode(',', $relIds) . ")", $relParamas);
1844 if ($recordsFound) {
1845 return FALSE;
1846 }
1847 return TRUE;
1848 }
1849
1850 $recordsFound = (int) CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM civicrm_relationship WHERE relationship_type_id IN ( " . implode(',', $membershipTypeRelationshipTypeIDs) . " ) AND contact_id_a IN ( %1, %2 ) AND contact_id_b IN ( %1, %2 ) AND id NOT IN (" . implode(',', $relIds) . ")", $relParamas);
1851
1852 if ($recordsFound) {
1853 return FALSE;
1854 }
1855
1856 return TRUE;
1857 }
1858
1859 /**
1860 * Get Current Employer for Contact.
1861 *
1862 * @param $contactIds
1863 * Contact Ids.
1864 *
1865 * @return array
1866 * array of the current employer
1867 */
1868 public static function getCurrentEmployer($contactIds) {
1869 $contacts = implode(',', $contactIds);
1870
1871 $query = "
1872 SELECT organization_name, id, employer_id
1873 FROM civicrm_contact
1874 WHERE id IN ( {$contacts} )
1875 ";
1876
1877 $dao = CRM_Core_DAO::executeQuery($query);
1878 $currentEmployer = [];
1879 while ($dao->fetch()) {
1880 $currentEmployer[$dao->id]['org_id'] = $dao->employer_id;
1881 $currentEmployer[$dao->id]['org_name'] = $dao->organization_name;
1882 }
1883
1884 return $currentEmployer;
1885 }
1886
1887 /**
1888 * Function to return list of permissioned contacts for a given contact and relationship type.
1889 *
1890 * @param int $contactID
1891 * contact id whose permissioned contacts are to be found.
1892 * @param int $relTypeId
1893 * one or more relationship type id's.
1894 * @param string $name
1895 * @param string $contactType
1896 *
1897 * @return array
1898 * Array of contacts
1899 */
1900 public static function getPermissionedContacts($contactID, $relTypeId = NULL, $name = NULL, $contactType = NULL) {
1901 $contacts = [];
1902 $args = [1 => [$contactID, 'Integer']];
1903 $relationshipTypeClause = $contactTypeClause = '';
1904
1905 if ($relTypeId) {
1906 // @todo relTypeId is only ever passed in as an int. Change this to reflect that -
1907 // probably being overly conservative by not doing so but working on stable release.
1908 $relationshipTypeClause = 'AND cr.relationship_type_id IN (%2) ';
1909 $args[2] = [$relTypeId, 'String'];
1910 }
1911
1912 if ($contactType) {
1913 $contactTypeClause = ' AND cr.relationship_type_id = crt.id AND crt.contact_type_b = %3 ';
1914 $args[3] = [$contactType, 'String'];
1915 }
1916
1917 $query = "
1918 SELECT cc.id as id, cc.sort_name as name
1919 FROM civicrm_relationship cr, civicrm_contact cc, civicrm_relationship_type crt
1920 WHERE
1921 cr.contact_id_a = %1 AND
1922 cr.is_permission_a_b = 1 AND
1923 IF(cr.end_date IS NULL, 1, (DATEDIFF( CURDATE( ), cr.end_date ) <= 0)) AND
1924 cr.is_active = 1 AND
1925 cc.id = cr.contact_id_b AND
1926 cc.is_deleted = 0
1927 $relationshipTypeClause
1928 $contactTypeClause
1929 ";
1930
1931 if (!empty($name)) {
1932 $name = CRM_Utils_Type::escape($name, 'String');
1933 $query .= "
1934 AND cc.sort_name LIKE '%$name%'";
1935 }
1936
1937 $dao = CRM_Core_DAO::executeQuery($query, $args);
1938 while ($dao->fetch()) {
1939 $contacts[$dao->id] = [
1940 'name' => $dao->name,
1941 'value' => $dao->id,
1942 ];
1943 }
1944
1945 return $contacts;
1946 }
1947
1948 /**
1949 * Merge relationships from otherContact to mainContact.
1950 *
1951 * Called during contact merge operation
1952 *
1953 * @param int $mainId
1954 * Contact id of main contact record.
1955 * @param int $otherId
1956 * Contact id of record which is going to merge.
1957 * @param array $sqls
1958 * (reference) array of sql statements to append to.
1959 *
1960 * @see CRM_Dedupe_Merger::cpTables()
1961 */
1962 public static function mergeRelationships($mainId, $otherId, &$sqls) {
1963 // Delete circular relationships
1964 $sqls[] = "DELETE FROM civicrm_relationship
1965 WHERE (contact_id_a = $mainId AND contact_id_b = $otherId)
1966 OR (contact_id_b = $mainId AND contact_id_a = $otherId)";
1967
1968 // Delete relationship from other contact if main contact already has that relationship
1969 $sqls[] = "DELETE r2
1970 FROM civicrm_relationship r1, civicrm_relationship r2
1971 WHERE r1.relationship_type_id = r2.relationship_type_id
1972 AND r1.id <> r2.id
1973 AND (
1974 r1.contact_id_a = $mainId AND r2.contact_id_a = $otherId AND r1.contact_id_b = r2.contact_id_b
1975 OR r1.contact_id_b = $mainId AND r2.contact_id_b = $otherId AND r1.contact_id_a = r2.contact_id_a
1976 OR (
1977 (r1.contact_id_a = $mainId AND r2.contact_id_b = $otherId AND r1.contact_id_b = r2.contact_id_a
1978 OR r1.contact_id_b = $mainId AND r2.contact_id_a = $otherId AND r1.contact_id_a = r2.contact_id_b)
1979 AND r1.relationship_type_id IN (SELECT id FROM civicrm_relationship_type WHERE name_b_a = name_a_b)
1980 )
1981 )";
1982
1983 // Move relationships
1984 $sqls[] = "UPDATE IGNORE civicrm_relationship SET contact_id_a = $mainId WHERE contact_id_a = $otherId";
1985 $sqls[] = "UPDATE IGNORE civicrm_relationship SET contact_id_b = $mainId WHERE contact_id_b = $otherId";
1986
1987 // Move current employer id (name will get updated later)
1988 $sqls[] = "UPDATE civicrm_contact SET employer_id = $mainId WHERE employer_id = $otherId";
1989 }
1990
1991 /**
1992 * Set 'is_valid' field to false for all relationships whose end date is in the past, ie. are expired.
1993 *
1994 * @return bool
1995 * True on success, false if error is encountered.
1996 * @throws \CiviCRM_API3_Exception
1997 */
1998 public static function disableExpiredRelationships() {
1999 $query = "SELECT id FROM civicrm_relationship WHERE is_active = 1 AND end_date < CURDATE()";
2000
2001 $dao = CRM_Core_DAO::executeQuery($query);
2002 while ($dao->fetch()) {
2003 $result = CRM_Contact_BAO_Relationship::setIsActive($dao->id, FALSE);
2004 // Result will be NULL if error occurred. We abort early if error detected.
2005 if ($result == NULL) {
2006 return FALSE;
2007 }
2008 }
2009 return TRUE;
2010 }
2011
2012 /**
2013 * Function filters the query by possible relationships for the membership type.
2014 *
2015 * It is intended to be called when constructing queries for the api (reciprocal & non-reciprocal)
2016 * and to add clauses to limit the return to those relationships which COULD inherit a membership type
2017 * (as opposed to those who inherit a particular membership
2018 *
2019 * @param array $params
2020 * Api input array.
2021 * @param null $direction
2022 *
2023 * @return array|void
2024 * @throws \CiviCRM_API3_Exception
2025 */
2026 public static function membershipTypeToRelationshipTypes(&$params, $direction = NULL) {
2027 $membershipType = civicrm_api3('membership_type', 'getsingle', [
2028 'id' => $params['membership_type_id'],
2029 'return' => 'relationship_type_id, relationship_direction',
2030 ]);
2031 $relationshipTypes = $membershipType['relationship_type_id'];
2032 if (empty($relationshipTypes)) {
2033 return NULL;
2034 }
2035 // if we don't have any contact data we can only filter on type
2036 if (empty($params['contact_id']) && empty($params['contact_id_a']) && empty($params['contact_id_a'])) {
2037 $params['relationship_type_id'] = ['IN' => $relationshipTypes];
2038 return NULL;
2039 }
2040 else {
2041 $relationshipDirections = (array) $membershipType['relationship_direction'];
2042 // if we have contact_id_a OR contact_id_b we can make a call here
2043 // if we have contact??
2044 foreach ($relationshipDirections as $index => $mtdirection) {
2045 if (isset($params['contact_id_a']) && $mtdirection == 'a_b' || $direction == 'a_b') {
2046 $types[] = $relationshipTypes[$index];
2047 }
2048 if (isset($params['contact_id_b']) && $mtdirection == 'b_a' || $direction == 'b_a') {
2049 $types[] = $relationshipTypes[$index];
2050 }
2051 }
2052 if (!empty($types)) {
2053 $params['relationship_type_id'] = ['IN' => $types];
2054 }
2055 elseif (!empty($clauses)) {
2056 return explode(' OR ', $clauses);
2057 }
2058 else {
2059 // effectively setting it to return no results
2060 $params['relationship_type_id'] = 0;
2061 }
2062 }
2063 }
2064
2065 /**
2066 * Wrapper for contact relationship selector.
2067 *
2068 * @param array $params
2069 * Associated array for params record id.
2070 *
2071 * @return array
2072 * associated array of contact relationships
2073 * @throws \Exception
2074 */
2075 public static function getContactRelationshipSelector(&$params) {
2076 // format the params
2077 $params['offset'] = ($params['page'] - 1) * $params['rp'];
2078 $params['sort'] = CRM_Utils_Array::value('sortBy', $params);
2079
2080 if ($params['context'] == 'past') {
2081 $relationshipStatus = CRM_Contact_BAO_Relationship::INACTIVE;
2082 }
2083 elseif ($params['context'] == 'all') {
2084 $relationshipStatus = CRM_Contact_BAO_Relationship::ALL;
2085 }
2086 else {
2087 $relationshipStatus = CRM_Contact_BAO_Relationship::CURRENT;
2088 }
2089
2090 // check logged in user for permission
2091 $page = new CRM_Core_Page();
2092 CRM_Contact_Page_View::checkUserPermission($page, $params['contact_id']);
2093 $permissions = [$page->_permission];
2094 if ($page->_permission == CRM_Core_Permission::EDIT) {
2095 $permissions[] = CRM_Core_Permission::DELETE;
2096 }
2097 $mask = CRM_Core_Action::mask($permissions);
2098
2099 $permissionedContacts = TRUE;
2100 if ($params['context'] != 'user') {
2101 $links = CRM_Contact_Page_View_Relationship::links();
2102 }
2103 else {
2104 $links = CRM_Contact_Page_View_UserDashBoard::links();
2105 $mask = NULL;
2106 }
2107 // get contact relationships
2108 $relationships = CRM_Contact_BAO_Relationship::getRelationship($params['contact_id'],
2109 $relationshipStatus,
2110 $params['rp'], 0, 0,
2111 $links, $mask,
2112 $permissionedContacts,
2113 $params, TRUE
2114 );
2115
2116 $contactRelationships = [];
2117 $params['total'] = $relationships['total_relationships'];
2118 unset($relationships['total_relationships']);
2119 if (!empty($relationships)) {
2120
2121 $displayName = CRM_Contact_BAO_Contact::displayName($params['contact_id']);
2122
2123 // format params
2124 foreach ($relationships as $relationshipId => $values) {
2125 $relationship = [];
2126
2127 $relationship['DT_RowId'] = $values['id'];
2128 $relationship['DT_RowClass'] = 'crm-entity';
2129 if ($values['is_active'] == 0) {
2130 $relationship['DT_RowClass'] .= ' disabled';
2131 }
2132
2133 $relationship['DT_RowAttr'] = [];
2134 $relationship['DT_RowAttr']['data-entity'] = 'relationship';
2135 $relationship['DT_RowAttr']['data-id'] = $values['id'];
2136
2137 //Add image icon for related contacts: CRM-14919; CRM-19668
2138 $contactType = (!empty($values['contact_sub_type'])) ? $values['contact_sub_type'] : $values['contact_type'];
2139 $icon = CRM_Contact_BAO_Contact_Utils::getImage($contactType,
2140 FALSE,
2141 $values['cid']
2142 );
2143 $relationship['sort_name'] = $icon . ' ' . CRM_Utils_System::href(
2144 $values['name'],
2145 'civicrm/contact/view',
2146 "reset=1&cid={$values['cid']}");
2147
2148 $relationship['relation'] = CRM_Utils_Array::value('case', $values, '') . CRM_Utils_System::href(
2149 $values['relation'],
2150 'civicrm/contact/view/rel',
2151 "action=view&reset=1&cid={$values['cid']}&id={$values['id']}&rtype={$values['rtype']}");
2152
2153 if (!empty($values['description'])) {
2154 $relationship['relation'] .= "<p class='description'>{$values['description']}</p>";
2155 }
2156
2157 if ($params['context'] == 'current') {
2158 $smarty = CRM_Core_Smarty::singleton();
2159
2160 $contactCombos = [
2161 [
2162 'permContact' => $params['contact_id'],
2163 'permDisplayName' => $displayName,
2164 'otherContact' => $values['cid'],
2165 'otherDisplayName' => $values['display_name'],
2166 'columnKey' => 'sort_name',
2167 ],
2168 [
2169 'permContact' => $values['cid'],
2170 'permDisplayName' => $values['display_name'],
2171 'otherContact' => $params['contact_id'],
2172 'otherDisplayName' => $displayName,
2173 'columnKey' => 'relation',
2174 ],
2175 ];
2176
2177 foreach ($contactCombos as $combo) {
2178 foreach ([CRM_Contact_BAO_Relationship::EDIT, CRM_Contact_BAO_Relationship::VIEW] as $permType) {
2179 $smarty->assign('permType', $permType);
2180 if (($combo['permContact'] == $values['contact_id_a'] and $values['is_permission_a_b'] == $permType)
2181 || ($combo['permContact'] == $values['contact_id_b'] and $values['is_permission_b_a'] == $permType)
2182 ) {
2183 $smarty->assign('permDisplayName', $combo['permDisplayName']);
2184 $smarty->assign('otherDisplayName', $combo['otherDisplayName']);
2185 $relationship[$combo['columnKey']] .= $smarty->fetch('CRM/Contact/Page/View/RelationshipPerm.tpl');
2186 }
2187 }
2188 }
2189 }
2190
2191 $relationship['start_date'] = CRM_Utils_Date::customFormat($values['start_date']);
2192 $relationship['end_date'] = CRM_Utils_Date::customFormat($values['end_date']);
2193 $relationship['city'] = $values['city'];
2194 $relationship['state'] = $values['state'];
2195 $relationship['email'] = $values['email'];
2196 $relationship['phone'] = $values['phone'];
2197 $relationship['links'] = $values['action'];
2198
2199 array_push($contactRelationships, $relationship);
2200 }
2201 }
2202
2203 $relationshipsDT = [];
2204 $relationshipsDT['data'] = $contactRelationships;
2205 $relationshipsDT['recordsTotal'] = $params['total'];
2206 $relationshipsDT['recordsFiltered'] = $params['total'];
2207
2208 return $relationshipsDT;
2209 }
2210
2211 /**
2212 * @inheritdoc
2213 */
2214 public static function buildOptions($fieldName, $context = NULL, $props = []) {
2215 if ($fieldName === 'relationship_type_id') {
2216 return self::buildRelationshipTypeOptions($props);
2217 }
2218
2219 return parent::buildOptions($fieldName, $context, $props);
2220 }
2221
2222 /**
2223 * Builds a list of options available for relationship types
2224 *
2225 * @param array $params
2226 * - contact_type: Limits by contact type on the "A" side
2227 * - relationship_id: Used to find the value for contact type for "B" side.
2228 * If contact_a matches provided contact_id then type of contact_b will
2229 * be used. Otherwise uses type of contact_a. Must be used with contact_id
2230 * - contact_id: Limits by contact types of this contact on the "A" side
2231 * - is_form: Returns array with keys indexed for use in a quickform
2232 * - relationship_direction: For relationship types with duplicate names
2233 * on both sides, defines which option should be returned, a_b or b_a
2234 *
2235 * @return array
2236 */
2237 public static function buildRelationshipTypeOptions($params = []) {
2238 $contactId = CRM_Utils_Array::value('contact_id', $params);
2239 $direction = CRM_Utils_Array::value('relationship_direction', $params, 'a_b');
2240 $relationshipId = CRM_Utils_Array::value('relationship_id', $params);
2241 $contactType = CRM_Utils_Array::value('contact_type', $params);
2242 $isForm = CRM_Utils_Array::value('is_form', $params);
2243 $showAll = FALSE;
2244
2245 // getContactRelationshipType will return an empty set if these are not set
2246 if (!$contactId && !$relationshipId && !$contactType) {
2247 $showAll = TRUE;
2248 }
2249
2250 $labels = self::getContactRelationshipType(
2251 $contactId,
2252 $direction,
2253 $relationshipId,
2254 $contactType,
2255 $showAll,
2256 'label'
2257 );
2258
2259 if ($isForm) {
2260 return $labels;
2261 }
2262
2263 $names = self::getContactRelationshipType(
2264 $contactId,
2265 $direction,
2266 $relationshipId,
2267 $contactType,
2268 $showAll,
2269 'name'
2270 );
2271
2272 // ensure $names contains only entries in $labels
2273 $names = array_intersect_key($names, $labels);
2274
2275 $nameToLabels = array_combine($names, $labels);
2276
2277 return $nameToLabels;
2278 }
2279
2280 /**
2281 * Process the params from api, form and check if current
2282 * employer should be set or unset.
2283 *
2284 * @param array $params
2285 * @param int $relationshipId
2286 * @param int|null $updatedRelTypeID
2287 *
2288 * @return bool
2289 * TRUE if current employer needs to be cleared.
2290 * @throws \CiviCRM_API3_Exception
2291 */
2292 public static function isCurrentEmployerNeedingToBeCleared($params, $relationshipId, $updatedRelTypeID = NULL) {
2293 $existingTypeID = (int) CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Relationship', $relationshipId, 'relationship_type_id');
2294 $updatedRelTypeID = $updatedRelTypeID ? $updatedRelTypeID : $existingTypeID;
2295 $currentEmployerID = (int) civicrm_api3('Contact', 'getvalue', ['return' => 'current_employer_id', 'id' => $params['contact_id_a']]);
2296
2297 if ($currentEmployerID !== (int) $params['contact_id_b'] || !self::isRelationshipTypeCurrentEmployer($existingTypeID)) {
2298 return FALSE;
2299 }
2300 //Clear employer if relationship is expired.
2301 if (!empty($params['end_date']) && strtotime($params['end_date']) < time()) {
2302 return TRUE;
2303 }
2304 //current employer checkbox is disabled on the form.
2305 //inactive or relationship type(employer of) is updated.
2306 if ((isset($params['is_current_employer']) && empty($params['is_current_employer']))
2307 || ((isset($params['is_active']) && empty($params['is_active'])))
2308 || $existingTypeID != $updatedRelTypeID) {
2309 // If there are no other active employer relationships between the same 2 contacts...
2310 if (!civicrm_api3('Relationship', 'getcount', [
2311 'is_active' => 1,
2312 'relationship_type_id' => $existingTypeID,
2313 'id' => ['<>' => $params['id']],
2314 'contact_id_a' => $params['contact_id_a'],
2315 'contact_id_b' => $params['contact_id_b'],
2316 ])) {
2317 return TRUE;
2318 }
2319 }
2320
2321 return FALSE;
2322 }
2323
2324 /**
2325 * Is this a current employer relationship type.
2326 *
2327 * @todo - this could use cached pseudoconstant lookups.
2328 *
2329 * @param int $existingTypeID
2330 *
2331 * @return bool
2332 */
2333 private static function isRelationshipTypeCurrentEmployer(int $existingTypeID): bool {
2334 $isCurrentEmployerRelationshipType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', $existingTypeID, 'name_b_a') === 'Employer of';
2335 return $isCurrentEmployerRelationshipType;
2336 }
2337
2338 /**
2339 * Is the inherited relationship invalidated by this relationship change.
2340 *
2341 * @param $membershipValues
2342 * @param array $values
2343 * @param int $cid
2344 * @param int $mainRelatedContactId
2345 *
2346 * @return array
2347 * @throws \CiviCRM_API3_Exception
2348 */
2349 private static function isInheritedMembershipInvalidated($membershipValues, array $values, $cid, $mainRelatedContactId): array {
2350 $membershipType = CRM_Member_BAO_MembershipType::getMembershipType($membershipValues['membership_type_id']);
2351 $relTypeIds = $membershipType['relationship_type_id'];
2352 $membshipInheritedFrom = $membershipValues['owner_membership_id'] ?? NULL;
2353 if (!$membshipInheritedFrom || !in_array($values[$cid]['relationshipTypeId'], $relTypeIds)) {
2354 return [implode(',', $relTypeIds), FALSE];
2355 }
2356 //CRM-16300 check if owner membership exist for related membership
2357 $isDeletable = !empty($values[$mainRelatedContactId]['memberships'][$membshipInheritedFrom]);
2358 return [implode(',', $relTypeIds), $isDeletable];
2359 }
2360
2361 }