Merge pull request #22264 from sunilpawar/dev_recurring_error
[civicrm-core.git] / CRM / Contact / BAO / ContactType.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 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17 class CRM_Contact_BAO_ContactType extends CRM_Contact_DAO_ContactType implements \Civi\Test\HookInterface {
18
19 /**
20 * Fetch object based on array of properties.
21 *
22 * @param array $params
23 * (reference ) an assoc array of name/value pairs.
24 * @param array $defaults
25 * (reference ) an assoc array to hold the flattened values.
26 *
27 * @return CRM_Contact_DAO_ContactType|null
28 * object on success, null otherwise
29 */
30 public static function retrieve(&$params, &$defaults) {
31 $contactType = new CRM_Contact_DAO_ContactType();
32 $contactType->copyValues($params);
33 if ($contactType->find(TRUE)) {
34 CRM_Core_DAO::storeValues($contactType, $defaults);
35 return $contactType;
36 }
37 return NULL;
38 }
39
40 /**
41 * Is this contact type active.
42 *
43 * @param string $contactType
44 *
45 * @return bool
46 *
47 * @throws \API_Exception
48 */
49 public static function isActive($contactType) {
50 $contact = self::contactTypeInfo();
51 return array_key_exists($contactType, $contact);
52 }
53
54 /**
55 * Retrieve basic contact type information.
56 *
57 * @param bool $includeInactive
58 *
59 * @return array
60 * Array of basic contact types information.
61 *
62 * @throws \API_Exception
63 * @throws \Civi\API\Exception\UnauthorizedException
64 */
65 public static function basicTypeInfo($includeInactive = FALSE) {
66 return array_filter(self::getAllContactTypes(), function($type) use ($includeInactive) {
67 return empty($type['parent']) && ($includeInactive || $type['is_active']);
68 });
69 }
70
71 /**
72 * Retrieve all basic contact types.
73 *
74 * @param bool $all
75 *
76 * @return array
77 * Array of basic contact types
78 *
79 * @throws \API_Exception
80 * @throws \Civi\API\Exception\UnauthorizedException
81 */
82 public static function basicTypes($all = FALSE) {
83 return array_keys(self::basicTypeInfo($all));
84 }
85
86 /**
87 * @param bool $all
88 * @param string $key
89 *
90 * @return array
91 * @throws \API_Exception
92 * @throws \Civi\API\Exception\UnauthorizedException
93 */
94 public static function basicTypePairs($all = FALSE, $key = 'name') {
95 $subtypes = self::basicTypeInfo($all);
96
97 $pairs = [];
98 foreach ($subtypes as $name => $info) {
99 $index = ($key == 'name') ? $name : $info[$key];
100 $pairs[$index] = $info['label'];
101 }
102 return $pairs;
103 }
104
105 /**
106 * Retrieve all subtypes Information.
107 *
108 * @param array $contactType
109 * @param bool $all
110 *
111 * @return array
112 * Array of sub type information, subset of getAllContactTypes.
113 *
114 * @throws \API_Exception
115 */
116 public static function subTypeInfo($contactType = NULL, $all = FALSE) {
117 $contactTypes = self::getAllContactTypes();
118 foreach ($contactTypes as $index => $type) {
119 if (empty($type['parent']) ||
120 (!$all && !$type['is_active'])
121 || ($contactType && $type['parent'] !== $contactType)
122 ) {
123 unset($contactTypes[$index]);
124 }
125 }
126 return $contactTypes;
127 }
128
129 /**
130 *
131 * retrieve all subtypes
132 *
133 * @param array $contactType
134 * ..
135 * @param bool $all
136 * @param string $columnName
137 * @param bool $ignoreCache
138 *
139 * @return array
140 * all subtypes OR list of subtypes associated to
141 * a given basic contact type
142 * @throws \API_Exception
143 */
144 public static function subTypes($contactType = NULL, $all = FALSE, $columnName = 'name', $ignoreCache = FALSE) {
145 if ($columnName === 'name') {
146 return array_keys(self::subTypeInfo($contactType, $all, $ignoreCache));
147 }
148 else {
149 return array_values(self::subTypePairs($contactType, FALSE, NULL, $ignoreCache));
150 }
151 }
152
153 /**
154 *
155 * retrieve subtype pairs with name as 'subtype-name' and 'label' as value
156 *
157 * @param array $contactType
158 * @param bool $all
159 * @param string $labelPrefix
160 * @param bool $ignoreCache
161 *
162 * @return array
163 * list of subtypes with name as 'subtype-name' and 'label' as value
164 */
165 public static function subTypePairs($contactType = NULL, $all = FALSE, $labelPrefix = '- ', $ignoreCache = FALSE) {
166 $subtypes = self::subTypeInfo($contactType, $all, $ignoreCache);
167
168 $pairs = [];
169 foreach ($subtypes as $name => $info) {
170 $pairs[$name] = $labelPrefix . $info['label'];
171 }
172 return $pairs;
173 }
174
175 /**
176 * Retrieve list of all types i.e basic + subtypes.
177 *
178 * @param bool $all
179 *
180 * @return array
181 * Array of basic types + all subtypes.
182 */
183 public static function contactTypes($all = FALSE) {
184 return array_keys(self::contactTypeInfo($all));
185 }
186
187 /**
188 * Retrieve info array about all types i.e basic + subtypes.
189 *
190 * @todo deprecate calling this with $all = TRUE in favour of getAllContactTypes
191 * & ideally add getActiveContactTypes & call that from this fully
192 * deprecated function.
193 *
194 * @param bool $all
195 *
196 * @return array
197 * Array of basic types + all subtypes.
198 */
199 public static function contactTypeInfo($all = FALSE) {
200 $contactTypes = self::getAllContactTypes();
201 if (!$all) {
202 foreach ($contactTypes as $index => $value) {
203 if (!$value['is_active']) {
204 unset($contactTypes[$index]);
205 }
206 }
207 }
208 return $contactTypes;
209 }
210
211 /**
212 * Retrieve basic type pairs with name as 'built-in name' and 'label' as value.
213 *
214 * @param bool $all
215 * @param null $typeName
216 * @param null $delimiter
217 *
218 * @return array|string
219 * Array of basictypes with name as 'built-in name' and 'label' as value
220 * @throws \API_Exception
221 */
222 public static function contactTypePairs($all = FALSE, $typeName = NULL, $delimiter = NULL) {
223 $types = self::contactTypeInfo($all);
224
225 if ($typeName && !is_array($typeName)) {
226 $typeName = explode(CRM_Core_DAO::VALUE_SEPARATOR, trim($typeName, CRM_Core_DAO::VALUE_SEPARATOR));
227 }
228
229 $pairs = [];
230 if ($typeName) {
231 foreach ($typeName as $type) {
232 if (array_key_exists($type, $types)) {
233 $pairs[$type] = $types[$type]['label'];
234 }
235 }
236 }
237 else {
238 foreach ($types as $name => $info) {
239 $pairs[$name] = $info['label'];
240 }
241 }
242
243 return !$delimiter ? $pairs : implode($delimiter, $pairs);
244 }
245
246 /**
247 * Get a list of elements for select box.
248 * Note that this used to default to using the hex(01) character - which results in an invalid character being used in form fields
249 * which was not handled well be anything that loaded & resaved the html (outside core)
250 * The use of this separator is now explicit in the calling functions as a step towards it's removal
251 *
252 * @param bool $all
253 * @param bool $isSeparator
254 * @param string $separator
255 *
256 * @return mixed
257 */
258 public static function getSelectElements(
259 $all = FALSE,
260 $isSeparator = TRUE,
261 $separator = '__'
262 ) {
263 // @todo - use Cache class - ie like Civi::cache('contactTypes')
264 static $_cache = NULL;
265
266 if ($_cache === NULL) {
267 $_cache = [];
268 }
269
270 // @todo - call getAllContactTypes & return filtered results.
271 $argString = $all ? 'CRM_CT_GSE_1' : 'CRM_CT_GSE_0';
272 $argString .= $isSeparator ? '_1' : '_0';
273 $argString .= $separator;
274 $argString = CRM_Utils_Cache::cleanKey($argString);
275 if (!array_key_exists($argString, $_cache)) {
276 $cache = CRM_Utils_Cache::singleton();
277 $_cache[$argString] = $cache->get($argString);
278
279 if (!$_cache[$argString]) {
280 $_cache[$argString] = [];
281
282 $sql = '
283 SELECT c.name as child_name , c.label as child_label , c.id as child_id,
284 p.name as parent_name, p.label as parent_label, p.id as parent_id
285 FROM civicrm_contact_type c
286 LEFT JOIN civicrm_contact_type p ON ( c.parent_id = p.id )
287 WHERE ( c.name IS NOT NULL )
288 ';
289
290 if ($all === FALSE) {
291 $sql .= '
292 AND c.is_active = 1
293 AND ( p.is_active = 1 OR p.id IS NULL )
294 ';
295 }
296 $sql .= " ORDER BY c.id";
297
298 $values = [];
299 $dao = CRM_Core_DAO::executeQuery($sql);
300 while ($dao->fetch()) {
301 if (!empty($dao->parent_id)) {
302 $key = $isSeparator ? $dao->parent_name . $separator . $dao->child_name : $dao->child_name;
303 $label = "- {$dao->child_label}";
304 $pName = $dao->parent_name;
305 }
306 else {
307 $key = $dao->child_name;
308 $label = $dao->child_label;
309 $pName = $dao->child_name;
310 }
311
312 if (!isset($values[$pName])) {
313 $values[$pName] = [];
314 }
315 $values[$pName][] = ['key' => $key, 'label' => $label];
316 }
317
318 $selectElements = [];
319 foreach ($values as $pName => $elements) {
320 foreach ($elements as $element) {
321 $selectElements[$element['key']] = $element['label'];
322 }
323 }
324 $_cache[$argString] = $selectElements;
325
326 $cache->set($argString, $_cache[$argString]);
327 }
328 }
329 return $_cache[$argString];
330 }
331
332 /**
333 * Check if a given type is a subtype.
334 *
335 * @param string $subType
336 * Contact subType.
337 * @param bool $ignoreCache
338 *
339 * @return bool
340 * true if subType, false otherwise.
341 */
342 public static function isaSubType($subType, $ignoreCache = FALSE) {
343 return in_array($subType, self::subTypes(NULL, TRUE, 'name', $ignoreCache));
344 }
345
346 /**
347 * Retrieve the basic contact type associated with given subType.
348 *
349 * @param array|string $subType contact subType.
350 * @return array|string
351 */
352 public static function getBasicType($subType) {
353 // @todo - use Cache class - ie like Civi::cache('contactTypes')
354 static $_cache = NULL;
355 if ($_cache === NULL) {
356 $_cache = [];
357 }
358
359 $isArray = TRUE;
360 if ($subType && !is_array($subType)) {
361 $subType = [$subType];
362 $isArray = FALSE;
363 }
364 $argString = implode('_', $subType);
365
366 if (!array_key_exists($argString, $_cache)) {
367 $_cache[$argString] = [];
368
369 $sql = "
370 SELECT subtype.name as contact_subtype, type.name as contact_type
371 FROM civicrm_contact_type subtype
372 INNER JOIN civicrm_contact_type type ON ( subtype.parent_id = type.id )
373 WHERE subtype.name IN ('" . implode("','", $subType) . "' )";
374 $dao = CRM_Core_DAO::executeQuery($sql);
375 while ($dao->fetch()) {
376 if (!$isArray) {
377 $_cache[$argString] = $dao->contact_type;
378 break;
379 }
380 $_cache[$argString][$dao->contact_subtype] = $dao->contact_type;
381 }
382 }
383 return $_cache[$argString];
384 }
385
386 /**
387 * Suppress all subtypes present in given array.
388 *
389 * @param array $subTypes
390 * Contact subTypes.
391 * @param bool $ignoreCache
392 *
393 * @return array
394 * Array of suppressed subTypes.
395 */
396 public static function suppressSubTypes(&$subTypes, $ignoreCache = FALSE) {
397 $subTypes = array_diff($subTypes, self::subTypes(NULL, TRUE, 'name', $ignoreCache));
398 return $subTypes;
399 }
400
401 /**
402 * Verify if a given subtype is associated with a given basic contact type.
403 *
404 * @param string $subType
405 * Contact subType.
406 * @param string $contactType
407 * Contact Type.
408 * @param bool $ignoreCache
409 * @param string $columnName
410 *
411 * @return bool
412 * true if contact extends, false otherwise.
413 */
414 public static function isExtendsContactType($subType, $contactType, $ignoreCache = FALSE, $columnName = 'name') {
415 $subType = (array) CRM_Utils_Array::explodePadded($subType);
416 $subtypeList = self::subTypes($contactType, TRUE, $columnName, $ignoreCache);
417 $intersection = array_intersect($subType, $subtypeList);
418 return $subType == $intersection;
419 }
420
421 /**
422 * Create shortcuts menu for contactTypes.
423 *
424 * @return array
425 * of contactTypes
426 */
427 public static function getCreateNewList() {
428 $shortCuts = [];
429 //@todo FIXME - using the CRM_Core_DAO::VALUE_SEPARATOR creates invalid html - if you can find the form
430 // this is loaded onto then replace with something like '__' & test
431 $separator = CRM_Core_DAO::VALUE_SEPARATOR;
432 $contactTypes = self::getSelectElements(FALSE, TRUE, $separator);
433 foreach ($contactTypes as $key => $value) {
434 if ($key) {
435 $typeValue = explode(CRM_Core_DAO::VALUE_SEPARATOR, $key);
436 $cType = $typeValue['0'] ?? NULL;
437 $typeUrl = 'ct=' . $cType;
438 if ($csType = CRM_Utils_Array::value('1', $typeValue)) {
439 $typeUrl .= "&cst=$csType";
440 }
441 $shortCut = [
442 'path' => 'civicrm/contact/add',
443 'query' => "$typeUrl&reset=1",
444 'ref' => "new-$value",
445 'title' => $value,
446 ];
447 if ($csType = CRM_Utils_Array::value('1', $typeValue)) {
448 $shortCuts[$cType]['shortCuts'][] = $shortCut;
449 }
450 else {
451 $shortCuts[$cType] = $shortCut;
452 }
453 }
454 }
455 return $shortCuts;
456 }
457
458 /**
459 * Delete Contact SubTypes.
460 *
461 * @param int $contactTypeId
462 * ID of the Contact Subtype to be deleted.
463 * @deprecated
464 * @return bool
465 */
466 public static function del($contactTypeId) {
467 if (!$contactTypeId) {
468 return FALSE;
469 }
470 try {
471 static::deleteRecord(['id' => $contactTypeId]);
472 return TRUE;
473 }
474 catch (CRM_Core_Exception $e) {
475 return FALSE;
476 }
477 }
478
479 /**
480 * Callback for hook_civicrm_pre().
481 * @param \Civi\Core\Event\PreEvent $event
482 * @throws CRM_Core_Exception
483 */
484 public static function self_hook_civicrm_pre(\Civi\Core\Event\PreEvent $event) {
485 // Before deleting a contactType, check references by custom groups
486 if ($event->action === 'delete') {
487 $name = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_ContactType', $event->id);
488 $sep = CRM_Core_DAO::VALUE_SEPARATOR;
489 $custom = new CRM_Core_DAO_CustomGroup();
490 $custom->whereAdd("extends_entity_column_value LIKE '%{$sep}{$name}{$sep}%'");
491 if ($custom->find()) {
492 throw new CRM_Core_Exception(ts("You can not delete this contact type -- it is used by %1 custom field group(s). The custom fields must be deleted first.", [1 => $custom->N]));
493 }
494 }
495 }
496
497 /**
498 * Callback for hook_civicrm_post().
499 * @param \Civi\Core\Event\PostEvent $event
500 */
501 public static function self_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) {
502 if ($event->action === 'delete') {
503 $sep = CRM_Core_DAO::VALUE_SEPARATOR;
504 $subType = "$sep{$event->object->name}$sep";
505 // For contacts with just the one sub-type, set to null
506 $sql = "
507 UPDATE civicrm_contact SET contact_sub_type = NULL
508 WHERE contact_sub_type = '$subType'";
509 CRM_Core_DAO::executeQuery($sql);
510 // For contacts with multipe sub-types, remove this one
511 $sql = "
512 UPDATE civicrm_contact SET contact_sub_type = REPLACE(contact_sub_type, '$subType', '$sep')
513 WHERE contact_sub_type LIKE '%{$subType}%'";
514 CRM_Core_DAO::executeQuery($sql);
515
516 // remove navigation entry which was auto-created when this sub-type was added
517 \Civi\Api4\Navigation::delete(FALSE)
518 ->addWhere('name', '=', "New {$event->object->name}")
519 ->addWhere('url', 'LIKE', 'civicrm/contact/add%')
520 // Overide the default which limits to a single domain
521 ->addWhere('domain_id', '>', 0)
522 ->execute();
523
524 Civi::cache('contactTypes')->clear();
525 }
526 }
527
528 /**
529 * Add or update Contact SubTypes.
530 *
531 * @param array $params
532 * An assoc array of name/value pairs.
533 *
534 * @return object|void
535 * @throws \CRM_Core_Exception
536 */
537 public static function add($params) {
538
539 // label or name
540 if (empty($params['id']) && empty($params['label'])) {
541 // @todo consider throwing exception instead.
542 return NULL;
543 }
544 if (empty($params['id']) && empty($params['name'])) {
545 $params['name'] = ucfirst(CRM_Utils_String::munge($params['label']));
546 }
547 if (!empty($params['parent_id']) &&
548 !CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_ContactType', $params['parent_id'])
549 ) {
550 return NULL;
551 }
552
553 $contactType = new CRM_Contact_DAO_ContactType();
554 $contactType->copyValues($params);
555 $contactType->id = $params['id'] ?? NULL;
556
557 $contactType->save();
558 if ($contactType->find(TRUE)) {
559 $contactName = $contactType->name;
560 $contact = ucfirst($contactType->label);
561 $active = $contactType->is_active;
562 }
563
564 if (!empty($params['id'])) {
565 $newParams = [
566 'label' => ts("New %1", [1 => $contact]),
567 'is_active' => $contactType->is_active,
568 ];
569 CRM_Core_BAO_Navigation::processUpdate(['name' => "New $contactName"], $newParams);
570 }
571 else {
572 $name = self::getBasicType($contactName);
573 if (!$name) {
574 return NULL;
575 }
576 $value = ['name' => "New $name"];
577 CRM_Core_BAO_Navigation::retrieve($value, $navinfo);
578 $navigation = [
579 'label' => ts("New %1", [1 => $contact]),
580 'name' => "New $contactName",
581 'url' => "civicrm/contact/add?ct=$name&cst=$contactName&reset=1",
582 'permission' => 'add contacts',
583 'parent_id' => $navinfo['id'],
584 'is_active' => $active,
585 ];
586 CRM_Core_BAO_Navigation::add($navigation);
587 }
588 CRM_Core_BAO_Navigation::resetNavigation();
589 Civi::cache('contactTypes')->clear();
590
591 return $contactType;
592 }
593
594 /**
595 * Update the is_active flag in the db.
596 *
597 * @param int $id
598 * Id of the database record.
599 * @param bool $is_active
600 * Value we want to set the is_active field.
601 *
602 * @return bool
603 * true if we found and updated the object, else false
604 */
605 public static function setIsActive($id, $is_active) {
606 $params = ['id' => $id];
607 self::retrieve($params, $contactinfo);
608 $params = ['name' => "New $contactinfo[name]"];
609 $newParams = ['is_active' => $is_active];
610 CRM_Core_BAO_Navigation::processUpdate($params, $newParams);
611 CRM_Core_BAO_Navigation::resetNavigation();
612 return CRM_Core_DAO::setFieldValue('CRM_Contact_DAO_ContactType', $id,
613 'is_active', $is_active
614 );
615 }
616
617 /**
618 * @param string $typeName
619 *
620 * @return string
621 * @throws \API_Exception
622 */
623 public static function getLabel($typeName) {
624 $types = self::contactTypeInfo(TRUE);
625
626 if (array_key_exists($typeName, $types)) {
627 return $types[$typeName]['label'];
628 }
629 return $typeName;
630 }
631
632 /**
633 * Check whether allow to change any contact's subtype
634 * on the basis of custom data and relationship of specific subtype
635 * currently used in contact/edit form amd in import validation
636 *
637 * @param int $contactId
638 * Contact id.
639 * @param string $subType
640 * Subtype.
641 *
642 * @return bool
643 * @throws \CRM_Core_Exception
644 */
645 public static function isAllowEdit($contactId, $subType = NULL) {
646
647 if (!$contactId) {
648 return TRUE;
649 }
650
651 if (empty($subType)) {
652 $subType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact',
653 $contactId,
654 'contact_sub_type'
655 );
656 }
657
658 if (self::hasCustomData($subType, $contactId) || self::hasRelationships($contactId, $subType)) {
659 return FALSE;
660 }
661
662 return TRUE;
663 }
664
665 /**
666 * @param $contactType
667 * @param int $contactId
668 *
669 * @return bool
670 */
671 public static function hasCustomData($contactType, $contactId = NULL) {
672 $subTypeClause = '';
673
674 if (self::isaSubType($contactType)) {
675 $subType = $contactType;
676 $contactType = self::getBasicType($subType);
677
678 // check for empty custom data which extends subtype
679 $subTypeValue = CRM_Core_DAO::VALUE_SEPARATOR . $subType . CRM_Core_DAO::VALUE_SEPARATOR;
680 $subTypeClause = " AND extends_entity_column_value LIKE '%{$subTypeValue}%' ";
681 }
682 $query = "SELECT table_name FROM civicrm_custom_group WHERE extends = '{$contactType}' {$subTypeClause}";
683
684 $dao = CRM_Core_DAO::executeQuery($query);
685 while ($dao->fetch()) {
686 $sql = "SELECT count(id) FROM {$dao->table_name}";
687 if ($contactId) {
688 $sql .= " WHERE entity_id = {$contactId}";
689 }
690 $sql .= " LIMIT 1";
691
692 $customDataCount = CRM_Core_DAO::singleValueQuery($sql);
693 if (!empty($customDataCount)) {
694 return TRUE;
695 }
696 }
697 return FALSE;
698 }
699
700 /**
701 * @todo what does this function do?
702 * @param int $contactId
703 * @param $contactType
704 *
705 * @return bool
706 */
707 public static function hasRelationships($contactId, $contactType) {
708 if (self::isaSubType($contactType)) {
709 $subType = $contactType;
710 $contactType = self::getBasicType($subType);
711 $subTypeClause = " AND ( ( crt.contact_type_a = '{$contactType}' AND crt.contact_sub_type_a = '{$subType}') OR
712 ( crt.contact_type_b = '{$contactType}' AND crt.contact_sub_type_b = '{$subType}') ) ";
713 }
714 else {
715 $subTypeClause = " AND ( crt.contact_type_a = '{$contactType}' OR crt.contact_type_b = '{$contactType}' ) ";
716 }
717
718 // check relationships for
719 $relationshipQuery = "
720 SELECT count(cr.id) FROM civicrm_relationship cr
721 INNER JOIN civicrm_relationship_type crt ON
722 ( cr.relationship_type_id = crt.id {$subTypeClause} )
723 WHERE ( cr.contact_id_a = {$contactId} OR cr.contact_id_b = {$contactId} )
724 LIMIT 1";
725
726 $relationshipCount = CRM_Core_DAO::singleValueQuery($relationshipQuery);
727
728 if (!empty($relationshipCount)) {
729 return TRUE;
730 }
731
732 return FALSE;
733 }
734
735 /**
736 * @param $contactType
737 * @param array $subtypeSet
738 *
739 * @return array
740 * @throws \CRM_Core_Exception
741 * @todo what does this function do?
742 */
743 public static function getSubtypeCustomPair($contactType, $subtypeSet = []) {
744 if (empty($subtypeSet)) {
745 return $subtypeSet;
746 }
747
748 $customSet = $subTypeClause = [];
749 foreach ($subtypeSet as $subtype) {
750 $subtype = CRM_Utils_Type::escape($subtype, 'String');
751 $subtype = CRM_Core_DAO::VALUE_SEPARATOR . $subtype . CRM_Core_DAO::VALUE_SEPARATOR;
752 $subTypeClause[] = "extends_entity_column_value LIKE '%{$subtype}%' ";
753 }
754 $query = 'SELECT table_name
755 FROM civicrm_custom_group
756 WHERE extends = %1 AND ' . implode(" OR ", $subTypeClause);
757 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$contactType, 'String']]);
758 while ($dao->fetch()) {
759 $customSet[] = $dao->table_name;
760 }
761 return array_unique($customSet);
762 }
763
764 /**
765 * Function that does something.
766 *
767 * @param int $contactID
768 * @param string $contactType
769 * @param array $oldSubtypeSet
770 * @param array $newSubtypeSet
771 *
772 * @return bool
773 * @throws \CRM_Core_Exception
774 *
775 * @todo what does this function do?
776 */
777 public static function deleteCustomSetForSubtypeMigration(
778 $contactID,
779 $contactType,
780 $oldSubtypeSet = [],
781 $newSubtypeSet = []
782 ) {
783 $oldCustomSet = self::getSubtypeCustomPair($contactType, $oldSubtypeSet);
784 $newCustomSet = self::getSubtypeCustomPair($contactType, $newSubtypeSet);
785
786 $customToBeRemoved = array_diff($oldCustomSet, $newCustomSet);
787 foreach ($customToBeRemoved as $customTable) {
788 self::deleteCustomRowsForEntityID($customTable, $contactID);
789 }
790 return TRUE;
791 }
792
793 /**
794 * Delete content / rows of a custom table specific to a subtype for a given custom-group.
795 * This function currently works for contact subtypes only and could be later improved / genralized
796 * to work for other subtypes as well.
797 *
798 * @param int $gID
799 * Custom group id.
800 * @param array $subtypes
801 * List of subtypes related to which entry is to be removed.
802 * @param array $subtypesToPreserve
803 *
804 * @return bool
805 *
806 * @throws \CRM_Core_Exception
807 */
808 public static function deleteCustomRowsOfSubtype($gID, $subtypes = [], $subtypesToPreserve = []) {
809 if (!$gID or empty($subtypes)) {
810 return FALSE;
811 }
812
813 $tableName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $gID, 'table_name');
814
815 // drop triggers CRM-13587
816 CRM_Core_DAO::dropTriggers($tableName);
817
818 foreach ($subtypesToPreserve as $subtypeToPreserve) {
819 $subtypeToPreserve = CRM_Utils_Type::escape($subtypeToPreserve, 'String');
820 $subtypesToPreserveClause[] = "(civicrm_contact.contact_sub_type NOT LIKE '%" . CRM_Core_DAO::VALUE_SEPARATOR . $subtypeToPreserve . CRM_Core_DAO::VALUE_SEPARATOR . "%')";
821 }
822 $subtypesToPreserveClause = implode(' AND ', $subtypesToPreserveClause);
823
824 $subtypeClause = [];
825 foreach ($subtypes as $subtype) {
826 $subtype = CRM_Utils_Type::escape($subtype, 'String');
827 $subtypeClause[] = "( civicrm_contact.contact_sub_type LIKE '%" . CRM_Core_DAO::VALUE_SEPARATOR . $subtype . CRM_Core_DAO::VALUE_SEPARATOR . "%'"
828 . " AND " . $subtypesToPreserveClause . ")";
829 }
830 $subtypeClause = implode(' OR ', $subtypeClause);
831
832 $query = "DELETE custom.*
833 FROM {$tableName} custom
834 INNER JOIN civicrm_contact ON civicrm_contact.id = custom.entity_id
835 WHERE ($subtypeClause)";
836
837 CRM_Core_DAO::singleValueQuery($query);
838
839 // rebuild triggers CRM-13587
840 CRM_Core_DAO::triggerRebuild($tableName);
841 }
842
843 /**
844 * Delete content / rows of a custom table specific entity-id for a given custom-group table.
845 *
846 * @param int $customTable
847 * Custom table name.
848 * @param int $entityID
849 * Entity id.
850 *
851 * @return null|string
852 *
853 * @throws \CRM_Core_Exception
854 */
855 public static function deleteCustomRowsForEntityID($customTable, $entityID) {
856 $customTable = CRM_Utils_Type::escape($customTable, 'String');
857 $query = "DELETE FROM {$customTable} WHERE entity_id = %1";
858 return CRM_Core_DAO::singleValueQuery($query, [1 => [$entityID, 'Integer']]);
859 }
860
861 /**
862 * Get all contact types, leveraging caching.
863 *
864 * Note, this function is used within APIv4 Entity.get, so must use a
865 * SQL query instead of calling APIv4 to avoid an infinite loop.
866 *
867 * @return array
868 * @throws \API_Exception
869 */
870 protected static function getAllContactTypes() {
871 $cache = Civi::cache('contactTypes');
872 $cacheKey = 'all_' . $GLOBALS['tsLocale'];
873 $contactTypes = $cache->get($cacheKey);
874 if ($contactTypes === NULL) {
875 $query = CRM_Utils_SQL_Select::from('civicrm_contact_type');
876 $dao = CRM_Core_DAO::executeQuery($query->toSQL());
877 $contactTypes = array_column($dao->fetchAll(), NULL, 'name');
878 $name_options = self::buildOptions('parent_id', 'validate');
879 $label_options = self::buildOptions('parent_id', 'get');
880 foreach ($contactTypes as $id => $contactType) {
881 $contactTypes[$id]['parent'] = $contactType['parent_id'] ? $name_options[$contactType['parent_id']] : NULL;
882 $contactTypes[$id]['parent_label'] = $contactType['parent_id'] ? $label_options[$contactType['parent_id']] : NULL;
883 // Cast int/bool types.
884 $contactTypes[$id]['id'] = (int) $contactType['id'];
885 $contactTypes[$id]['parent_id'] = $contactType['parent_id'] ? (int) $contactType['parent_id'] : NULL;
886 $contactTypes[$id]['is_active'] = (bool) $contactType['is_active'];
887 $contactTypes[$id]['is_reserved'] = (bool) $contactType['is_reserved'];
888 }
889 $cache->set($cacheKey, $contactTypes);
890 }
891 return $contactTypes;
892 }
893
894 }