3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
12 use Civi\Api4\ContactType
;
17 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 class CRM_Contact_BAO_ContactType
extends CRM_Contact_DAO_ContactType
{
22 * Fetch object based on array of properties.
24 * @param array $params
25 * (reference ) an assoc array of name/value pairs.
26 * @param array $defaults
27 * (reference ) an assoc array to hold the flattened values.
29 * @return CRM_Contact_DAO_ContactType|null
30 * object on success, null otherwise
32 public static function retrieve(&$params, &$defaults) {
33 $contactType = new CRM_Contact_DAO_ContactType();
34 $contactType->copyValues($params);
35 if ($contactType->find(TRUE)) {
36 CRM_Core_DAO
::storeValues($contactType, $defaults);
43 * Is this contact type active.
45 * @param string $contactType
49 * @throws \API_Exception
51 public static function isActive($contactType) {
52 $contact = self
::contactTypeInfo();
53 return array_key_exists($contactType, $contact);
57 * Retrieve basic contact type information.
59 * @todo - call getAllContactTypes & return filtered results.
61 * @param bool $includeInactive
64 * Array of basic contact types information.
66 * @throws \API_Exception
67 * @throws \Civi\API\Exception\UnauthorizedException
69 public static function basicTypeInfo($includeInactive = FALSE) {
70 $cacheKey = 'CRM_CT_BTI_' . (int) $includeInactive;
71 if (!Civi
::cache('contactTypes')->has($cacheKey)) {
72 $contactType = ContactType
::get(FALSE)->setSelect(['*'])->addWhere('parent_id', 'IS NULL');
73 if ($includeInactive === FALSE) {
74 $contactType->addWhere('is_active', '=', 1);
76 Civi
::cache('contactTypes')->set($cacheKey, (array) $contactType->execute()->indexBy('name'));
78 return Civi
::cache('contactTypes')->get($cacheKey);
82 * Retrieve all basic contact types.
87 * Array of basic contact types
89 * @throws \API_Exception
90 * @throws \Civi\API\Exception\UnauthorizedException
92 public static function basicTypes($all = FALSE) {
93 return array_keys(self
::basicTypeInfo($all));
101 * @throws \API_Exception
102 * @throws \Civi\API\Exception\UnauthorizedException
104 public static function basicTypePairs($all = FALSE, $key = 'name') {
105 $subtypes = self
::basicTypeInfo($all);
108 foreach ($subtypes as $name => $info) {
109 $index = ($key == 'name') ?
$name : $info[$key];
110 $pairs[$index] = $info['label'];
116 * Retrieve all subtypes Information.
118 * @param array $contactType
122 * Array of sub type information, subset of getAllContactTypes.
124 * @throws \API_Exception
126 public static function subTypeInfo($contactType = NULL, $all = FALSE) {
127 $contactTypes = self
::getAllContactTypes();
128 foreach ($contactTypes as $index => $type) {
129 if (empty($type['parent']) ||
130 (!$all && !$type['is_active'])
131 ||
($contactType && $type['parent'] !== $contactType)
133 unset($contactTypes[$index]);
136 return $contactTypes;
141 * retrieve all subtypes
143 * @param array $contactType
146 * @param string $columnName
147 * @param bool $ignoreCache
150 * all subtypes OR list of subtypes associated to
151 * a given basic contact type
152 * @throws \API_Exception
154 public static function subTypes($contactType = NULL, $all = FALSE, $columnName = 'name', $ignoreCache = FALSE) {
155 if ($columnName === 'name') {
156 return array_keys(self
::subTypeInfo($contactType, $all, $ignoreCache));
159 return array_values(self
::subTypePairs($contactType, FALSE, NULL, $ignoreCache));
165 * retrieve subtype pairs with name as 'subtype-name' and 'label' as value
167 * @param array $contactType
169 * @param string $labelPrefix
170 * @param bool $ignoreCache
173 * list of subtypes with name as 'subtype-name' and 'label' as value
175 public static function subTypePairs($contactType = NULL, $all = FALSE, $labelPrefix = '- ', $ignoreCache = FALSE) {
176 $subtypes = self
::subTypeInfo($contactType, $all, $ignoreCache);
179 foreach ($subtypes as $name => $info) {
180 $pairs[$name] = $labelPrefix . $info['label'];
187 * retrieve list of all types i.e basic + subtypes.
192 * Array of basic types + all subtypes.
194 public static function contactTypes($all = FALSE) {
195 return array_keys(self
::contactTypeInfo($all));
199 * Retrieve info array about all types i.e basic + subtypes.
201 * @todo deprecate calling this with $all = TRUE in favour of getAllContactTypes
202 * & ideally add getActiveContactTypes & call that from this fully
203 * deprecated function.
208 * Array of basic types + all subtypes.
209 * @throws \API_Exception
211 public static function contactTypeInfo($all = FALSE) {
212 $contactTypes = self
::getAllContactTypes();
214 foreach ($contactTypes as $index => $value) {
215 if (!$value['is_active']) {
216 unset($contactTypes[$index]);
220 return $contactTypes;
224 * Retrieve basic type pairs with name as 'built-in name' and 'label' as value.
227 * @param null $typeName
228 * @param null $delimiter
231 * Array of basictypes with name as 'built-in name' and 'label' as value
232 * @throws \API_Exception
234 public static function contactTypePairs($all = FALSE, $typeName = NULL, $delimiter = NULL) {
235 $types = self
::contactTypeInfo($all);
237 if ($typeName && !is_array($typeName)) {
238 $typeName = explode(CRM_Core_DAO
::VALUE_SEPARATOR
, trim($typeName, CRM_Core_DAO
::VALUE_SEPARATOR
));
243 foreach ($typeName as $type) {
244 if (array_key_exists($type, $types)) {
245 $pairs[$type] = $types[$type]['label'];
250 foreach ($types as $name => $info) {
251 $pairs[$name] = $info['label'];
255 return !$delimiter ?
$pairs : implode($delimiter, $pairs);
259 * Get a list of elements for select box.
260 * Note that this used to default to using the hex(01) character - which results in an invalid character being used in form fields
261 * which was not handled well be anything that loaded & resaved the html (outside core)
262 * The use of this separator is now explicit in the calling functions as a step towards it's removal
265 * @param bool $isSeparator
266 * @param string $separator
270 public static function getSelectElements(
275 // @todo - use Cache class - ie like Civi::cache('contactTypes')
276 static $_cache = NULL;
278 if ($_cache === NULL) {
282 // @todo - call getAllContactTypes & return filtered results.
283 $argString = $all ?
'CRM_CT_GSE_1' : 'CRM_CT_GSE_0';
284 $argString .= $isSeparator ?
'_1' : '_0';
285 $argString .= $separator;
286 $argString = CRM_Utils_Cache
::cleanKey($argString);
287 if (!array_key_exists($argString, $_cache)) {
288 $cache = CRM_Utils_Cache
::singleton();
289 $_cache[$argString] = $cache->get($argString);
291 if (!$_cache[$argString]) {
292 $_cache[$argString] = [];
295 SELECT c.name as child_name , c.label as child_label , c.id as child_id,
296 p.name as parent_name, p.label as parent_label, p.id as parent_id
297 FROM civicrm_contact_type c
298 LEFT JOIN civicrm_contact_type p ON ( c.parent_id = p.id )
299 WHERE ( c.name IS NOT NULL )
302 if ($all === FALSE) {
305 AND ( p.is_active = 1 OR p.id IS NULL )
308 $sql .= " ORDER BY c.id";
311 $dao = CRM_Core_DAO
::executeQuery($sql);
312 while ($dao->fetch()) {
313 if (!empty($dao->parent_id
)) {
314 $key = $isSeparator ?
$dao->parent_name
. $separator . $dao->child_name
: $dao->child_name
;
315 $label = "- {$dao->child_label}";
316 $pName = $dao->parent_name
;
319 $key = $dao->child_name
;
320 $label = $dao->child_label
;
321 $pName = $dao->child_name
;
324 if (!isset($values[$pName])) {
325 $values[$pName] = [];
327 $values[$pName][] = ['key' => $key, 'label' => $label];
330 $selectElements = [];
331 foreach ($values as $pName => $elements) {
332 foreach ($elements as $element) {
333 $selectElements[$element['key']] = $element['label'];
336 $_cache[$argString] = $selectElements;
338 $cache->set($argString, $_cache[$argString]);
341 return $_cache[$argString];
345 * Check if a given type is a subtype.
347 * @param string $subType
349 * @param bool $ignoreCache
352 * true if subType, false otherwise.
354 public static function isaSubType($subType, $ignoreCache = FALSE) {
355 return in_array($subType, self
::subTypes(NULL, TRUE, 'name', $ignoreCache));
359 * Retrieve the basic contact type associated with given subType.
361 * @param array|string $subType contact subType.
362 * @return array|string
365 public static function getBasicType($subType) {
366 // @todo - use Cache class - ie like Civi::cache('contactTypes')
367 static $_cache = NULL;
368 if ($_cache === NULL) {
373 if ($subType && !is_array($subType)) {
374 $subType = [$subType];
377 $argString = implode('_', $subType);
379 if (!array_key_exists($argString, $_cache)) {
380 $_cache[$argString] = [];
383 SELECT subtype.name as contact_subtype, type.name as contact_type
384 FROM civicrm_contact_type subtype
385 INNER JOIN civicrm_contact_type type ON ( subtype.parent_id = type.id )
386 WHERE subtype.name IN ('" . implode("','", $subType) . "' )";
387 $dao = CRM_Core_DAO
::executeQuery($sql);
388 while ($dao->fetch()) {
390 $_cache[$argString] = $dao->contact_type
;
393 $_cache[$argString][$dao->contact_subtype
] = $dao->contact_type
;
396 return $_cache[$argString];
400 * Suppress all subtypes present in given array.
402 * @param array $subTypes
404 * @param bool $ignoreCache
407 * Array of suppressed subTypes.
409 public static function suppressSubTypes(&$subTypes, $ignoreCache = FALSE) {
410 $subTypes = array_diff($subTypes, self
::subTypes(NULL, TRUE, 'name', $ignoreCache));
415 * Verify if a given subtype is associated with a given basic contact type.
417 * @param string $subType
419 * @param string $contactType
421 * @param bool $ignoreCache
422 * @param string $columnName
425 * true if contact extends, false otherwise.
427 public static function isExtendsContactType($subType, $contactType, $ignoreCache = FALSE, $columnName = 'name') {
428 $subType = (array) CRM_Utils_Array
::explodePadded($subType);
429 $subtypeList = self
::subTypes($contactType, TRUE, $columnName, $ignoreCache);
430 $intersection = array_intersect($subType, $subtypeList);
431 return $subType == $intersection;
435 * Create shortcuts menu for contactTypes.
440 public static function getCreateNewList() {
442 //@todo FIXME - using the CRM_Core_DAO::VALUE_SEPARATOR creates invalid html - if you can find the form
443 // this is loaded onto then replace with something like '__' & test
444 $separator = CRM_Core_DAO
::VALUE_SEPARATOR
;
445 $contactTypes = self
::getSelectElements(FALSE, TRUE, $separator);
446 foreach ($contactTypes as $key => $value) {
448 $typeValue = explode(CRM_Core_DAO
::VALUE_SEPARATOR
, $key);
449 $cType = $typeValue['0'] ??
NULL;
450 $typeUrl = 'ct=' . $cType;
451 if ($csType = CRM_Utils_Array
::value('1', $typeValue)) {
452 $typeUrl .= "&cst=$csType";
455 'path' => 'civicrm/contact/add',
456 'query' => "$typeUrl&reset=1",
457 'ref' => "new-$value",
460 if ($csType = CRM_Utils_Array
::value('1', $typeValue)) {
461 $shortCuts[$cType]['shortCuts'][] = $shortCut;
464 $shortCuts[$cType] = $shortCut;
472 * Delete Contact SubTypes.
474 * @param int $contactTypeId
475 * ID of the Contact Subtype to be deleted.
479 public static function del($contactTypeId) {
481 if (!$contactTypeId) {
485 $params = ['id' => $contactTypeId];
486 self
::retrieve($params, $typeInfo);
487 $name = $typeInfo['name'];
488 // check if any custom group
489 $custom = new CRM_Core_DAO_CustomGroup();
490 $custom->whereAdd("extends_entity_column_value LIKE '%" .
491 CRM_Core_DAO
::VALUE_SEPARATOR
.
493 CRM_Core_DAO
::VALUE_SEPARATOR
. "%'"
495 if ($custom->find()) {
499 // remove subtype for existing contacts
501 UPDATE civicrm_contact SET contact_sub_type = NULL
502 WHERE contact_sub_type = '$name'";
503 CRM_Core_DAO
::executeQuery($sql);
505 // remove subtype from contact type table
506 $contactType = new CRM_Contact_DAO_ContactType();
507 $contactType->id
= $contactTypeId;
508 $contactType->delete();
510 // remove navigation entry if any
514 FROM civicrm_navigation
516 $params = [1 => ["New $name", 'String']];
517 CRM_Core_DAO
::executeQuery($sql, $params);
518 CRM_Core_BAO_Navigation
::resetNavigation();
519 Civi
::cache('contactTypes')->clear();
525 * Add or update Contact SubTypes.
527 * @param array $params
528 * An assoc array of name/value pairs.
530 * @return object|void
531 * @throws \CRM_Core_Exception
533 public static function add($params) {
536 if (empty($params['id']) && empty($params['label'])) {
537 // @todo consider throwing exception instead.
540 if (empty($params['id']) && empty($params['name'])) {
541 $params['name'] = ucfirst(CRM_Utils_String
::munge($params['label']));
543 if (!empty($params['parent_id']) &&
544 !CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_ContactType', $params['parent_id'])
549 $contactType = new CRM_Contact_DAO_ContactType();
550 $contactType->copyValues($params);
551 $contactType->id
= $params['id'] ??
NULL;
553 $contactType->save();
554 if ($contactType->find(TRUE)) {
555 $contactName = $contactType->name
;
556 $contact = ucfirst($contactType->label
);
557 $active = $contactType->is_active
;
560 if (!empty($params['id'])) {
562 'label' => ts("New %1", [1 => $contact]),
563 'is_active' => $contactType->is_active
,
565 CRM_Core_BAO_Navigation
::processUpdate(['name' => "New $contactName"], $newParams);
568 $name = self
::getBasicType($contactName);
572 $value = ['name' => "New $name"];
573 CRM_Core_BAO_Navigation
::retrieve($value, $navinfo);
575 'label' => ts("New %1", [1 => $contact]),
576 'name' => "New $contactName",
577 'url' => "civicrm/contact/add?ct=$name&cst=$contactName&reset=1",
578 'permission' => 'add contacts',
579 'parent_id' => $navinfo['id'],
580 'is_active' => $active,
582 CRM_Core_BAO_Navigation
::add($navigation);
584 CRM_Core_BAO_Navigation
::resetNavigation();
585 Civi
::cache('contactTypes')->clear();
591 * Update the is_active flag in the db.
594 * Id of the database record.
595 * @param bool $is_active
596 * Value we want to set the is_active field.
599 * true if we found and updated the object, else false
601 public static function setIsActive($id, $is_active) {
602 $params = ['id' => $id];
603 self
::retrieve($params, $contactinfo);
604 $params = ['name' => "New $contactinfo[name]"];
605 $newParams = ['is_active' => $is_active];
606 CRM_Core_BAO_Navigation
::processUpdate($params, $newParams);
607 CRM_Core_BAO_Navigation
::resetNavigation();
608 return CRM_Core_DAO
::setFieldValue('CRM_Contact_DAO_ContactType', $id,
609 'is_active', $is_active
614 * @param string $typeName
617 * @throws \API_Exception
619 public static function getLabel($typeName) {
620 $types = self
::contactTypeInfo(TRUE);
622 if (array_key_exists($typeName, $types)) {
623 return $types[$typeName]['label'];
629 * Check whether allow to change any contact's subtype
630 * on the basis of custom data and relationship of specific subtype
631 * currently used in contact/edit form amd in import validation
633 * @param int $contactId
635 * @param string $subType
639 * @throws \CRM_Core_Exception
641 public static function isAllowEdit($contactId, $subType = NULL) {
647 if (empty($subType)) {
648 $subType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact',
654 if (self
::hasCustomData($subType, $contactId) || self
::hasRelationships($contactId, $subType)) {
662 * @param $contactType
663 * @param int $contactId
667 public static function hasCustomData($contactType, $contactId = NULL) {
670 if (self
::isaSubType($contactType)) {
671 $subType = $contactType;
672 $contactType = self
::getBasicType($subType);
674 // check for empty custom data which extends subtype
675 $subTypeValue = CRM_Core_DAO
::VALUE_SEPARATOR
. $subType . CRM_Core_DAO
::VALUE_SEPARATOR
;
676 $subTypeClause = " AND extends_entity_column_value LIKE '%{$subTypeValue}%' ";
678 $query = "SELECT table_name FROM civicrm_custom_group WHERE extends = '{$contactType}' {$subTypeClause}";
680 $dao = CRM_Core_DAO
::executeQuery($query);
681 while ($dao->fetch()) {
682 $sql = "SELECT count(id) FROM {$dao->table_name}";
684 $sql .= " WHERE entity_id = {$contactId}";
688 $customDataCount = CRM_Core_DAO
::singleValueQuery($sql);
689 if (!empty($customDataCount)) {
697 * @todo what does this function do?
698 * @param int $contactId
699 * @param $contactType
703 public static function hasRelationships($contactId, $contactType) {
704 $subTypeClause = NULL;
705 if (self
::isaSubType($contactType)) {
706 $subType = $contactType;
707 $contactType = self
::getBasicType($subType);
708 $subTypeClause = " AND ( ( crt.contact_type_a = '{$contactType}' AND crt.contact_sub_type_a = '{$subType}') OR
709 ( crt.contact_type_b = '{$contactType}' AND crt.contact_sub_type_b = '{$subType}') ) ";
712 $subTypeClause = " AND ( crt.contact_type_a = '{$contactType}' OR crt.contact_type_b = '{$contactType}' ) ";
715 // check relationships for
716 $relationshipQuery = "
717 SELECT count(cr.id) FROM civicrm_relationship cr
718 INNER JOIN civicrm_relationship_type crt ON
719 ( cr.relationship_type_id = crt.id {$subTypeClause} )
720 WHERE ( cr.contact_id_a = {$contactId} OR cr.contact_id_b = {$contactId} )
723 $relationshipCount = CRM_Core_DAO
::singleValueQuery($relationshipQuery);
725 if (!empty($relationshipCount)) {
733 * @param $contactType
734 * @param array $subtypeSet
737 * @throws \CRM_Core_Exception
738 * @todo what does this function do?
740 public static function getSubtypeCustomPair($contactType, $subtypeSet = []) {
741 if (empty($subtypeSet)) {
745 $customSet = $subTypeClause = [];
746 foreach ($subtypeSet as $subtype) {
747 $subtype = CRM_Utils_Type
::escape($subtype, 'String');
748 $subtype = CRM_Core_DAO
::VALUE_SEPARATOR
. $subtype . CRM_Core_DAO
::VALUE_SEPARATOR
;
749 $subTypeClause[] = "extends_entity_column_value LIKE '%{$subtype}%' ";
751 $query = 'SELECT table_name
752 FROM civicrm_custom_group
753 WHERE extends = %1 AND ' . implode(" OR ", $subTypeClause);
754 $dao = CRM_Core_DAO
::executeQuery($query, [1 => [$contactType, 'String']]);
755 while ($dao->fetch()) {
756 $customSet[] = $dao->table_name
;
758 return array_unique($customSet);
762 * Function that does something.
764 * @param int $contactID
765 * @param string $contactType
766 * @param array $oldSubtypeSet
767 * @param array $newSubtypeSet
770 * @throws \CRM_Core_Exception
772 * @todo what does this function do?
774 public static function deleteCustomSetForSubtypeMigration(
780 $oldCustomSet = self
::getSubtypeCustomPair($contactType, $oldSubtypeSet);
781 $newCustomSet = self
::getSubtypeCustomPair($contactType, $newSubtypeSet);
783 $customToBeRemoved = array_diff($oldCustomSet, $newCustomSet);
784 foreach ($customToBeRemoved as $customTable) {
785 self
::deleteCustomRowsForEntityID($customTable, $contactID);
791 * Delete content / rows of a custom table specific to a subtype for a given custom-group.
792 * This function currently works for contact subtypes only and could be later improved / genralized
793 * to work for other subtypes as well.
797 * @param array $subtypes
798 * List of subtypes related to which entry is to be removed.
799 * @param array $subtypesToPreserve
803 * @throws \CRM_Core_Exception
805 public static function deleteCustomRowsOfSubtype($gID, $subtypes = [], $subtypesToPreserve = []) {
806 if (!$gID or empty($subtypes)) {
810 $tableName = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_CustomGroup', $gID, 'table_name');
812 // drop triggers CRM-13587
813 CRM_Core_DAO
::dropTriggers($tableName);
815 foreach ($subtypesToPreserve as $subtypeToPreserve) {
816 $subtypeToPreserve = CRM_Utils_Type
::escape($subtypeToPreserve, 'String');
817 $subtypesToPreserveClause[] = "(civicrm_contact.contact_sub_type NOT LIKE '%" . CRM_Core_DAO
::VALUE_SEPARATOR
. $subtypeToPreserve . CRM_Core_DAO
::VALUE_SEPARATOR
. "%')";
819 $subtypesToPreserveClause = implode(' AND ', $subtypesToPreserveClause);
822 foreach ($subtypes as $subtype) {
823 $subtype = CRM_Utils_Type
::escape($subtype, 'String');
824 $subtypeClause[] = "( civicrm_contact.contact_sub_type LIKE '%" . CRM_Core_DAO
::VALUE_SEPARATOR
. $subtype . CRM_Core_DAO
::VALUE_SEPARATOR
. "%'"
825 . " AND " . $subtypesToPreserveClause . ")";
827 $subtypeClause = implode(' OR ', $subtypeClause);
829 $query = "DELETE custom.*
830 FROM {$tableName} custom
831 INNER JOIN civicrm_contact ON civicrm_contact.id = custom.entity_id
832 WHERE ($subtypeClause)";
834 CRM_Core_DAO
::singleValueQuery($query);
836 // rebuild triggers CRM-13587
837 CRM_Core_DAO
::triggerRebuild($tableName);
841 * Delete content / rows of a custom table specific entity-id for a given custom-group table.
843 * @param int $customTable
845 * @param int $entityID
848 * @return null|string
850 * @throws \CRM_Core_Exception
852 public static function deleteCustomRowsForEntityID($customTable, $entityID) {
853 $customTable = CRM_Utils_Type
::escape($customTable, 'String');
854 $query = "DELETE FROM {$customTable} WHERE entity_id = %1";
855 return CRM_Core_DAO
::singleValueQuery($query, [1 => [$entityID, 'Integer']]);
859 * Get all contact types, leveraging caching.
863 * @throws \API_Exception
865 protected static function getAllContactTypes() {
866 if (!Civi
::cache('contactTypes')->has('all')) {
867 $contactTypes = (array) ContactType
::get(FALSE)
868 ->setSelect(['id', 'name', 'label', 'description', 'is_active', 'is_reserved', 'image_URL', 'parent_id', 'parent_id:name', 'parent_id:label'])
869 ->execute()->indexBy('name');
871 foreach ($contactTypes as $id => $contactType) {
872 $contactTypes[$id]['parent'] = $contactType['parent_id:name'];
873 $contactTypes[$id]['parent_label'] = $contactType['parent_id:label'];
874 unset($contactTypes[$id]['parent_id:name'], $contactTypes[$id]['parent_id:label']);
876 Civi
::cache('contactTypes')->set('all', $contactTypes);
878 $contactTypes = Civi
::cache('contactTypes')->get('all');
879 return $contactTypes;