Merge pull request #21965 from civicrm/5.43
[civicrm-core.git] / CRM / Core / BAO / CustomValueTable.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
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 |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
16 */
17class CRM_Core_BAO_CustomValueTable {
18
b5c2afd0 19 /**
100fef9d 20 * @param array $customParams
46fe0a66 21 * @param string $parentOperation Operation being taken on the parent entity.
22 * If we know the parent entity is doing an insert we can skip the
23 * ON DUPLICATE UPDATE - which improves performance and reduces deadlocks.
24 * - edit
25 * - create
b5c2afd0
EM
26 *
27 * @throws Exception
28 */
46fe0a66 29 public static function create($customParams, $parentOperation = NULL) {
6a488035
TO
30 if (empty($customParams) ||
31 !is_array($customParams)
32 ) {
33 return;
34 }
8ef12e64 35
be2fb01f 36 $paramFieldsExtendContactForEntities = [];
a61d2ab7 37 $VS = CRM_Core_DAO::VALUE_SEPARATOR;
63b7d442 38
6a488035 39 foreach ($customParams as $tableName => $tables) {
3d89c1a4 40 foreach ($tables as $fields) {
353ffa53 41 $hookID = NULL;
353ffa53 42 $entityID = NULL;
be2fb01f
CW
43 $set = [];
44 $params = [];
353ffa53 45 $count = 1;
6a488035 46
048b069c 47 $firstField = reset($fields);
445bbeed 48 $entityID = (int) $firstField['entity_id'];
048b069c
BS
49 $isMultiple = $firstField['is_multiple'];
50 if (array_key_exists('id', $firstField)) {
51 $sqlOP = "UPDATE $tableName ";
52 $where = " WHERE id = %{$count}";
53 $params[$count] = [$firstField['id'], 'Integer'];
54 $count++;
55 $hookOP = 'edit';
56 }
57 else {
58 $sqlOP = "INSERT INTO $tableName ";
59 $where = NULL;
60 $hookOP = 'create';
61 }
62
445bbeed
EM
63 CRM_Utils_Hook::customPre(
64 $hookOP,
65 (int) $firstField['custom_group_id'],
048b069c
BS
66 $entityID,
67 $fields
68 );
69
70 foreach ($fields as $field) {
6a488035
TO
71 // fix the value before we store it
72 $value = $field['value'];
73 $type = $field['type'];
74 switch ($type) {
75 case 'StateProvince':
76 $type = 'Integer';
77 if (is_array($value)) {
78 $value = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $value) . CRM_Core_DAO::VALUE_SEPARATOR;
79 $type = 'String';
80 }
ec28b24d 81 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO::VALUE_SEPARATOR)) {
6a488035
TO
82 //fix for multi select state, CRM-3437
83 $mulValues = explode(',', $value);
be2fb01f 84 $validStates = [];
6a488035 85 foreach ($mulValues as $key => $stateVal) {
be2fb01f 86 $states = [];
6a488035
TO
87 $states['state_province'] = trim($stateVal);
88
89 CRM_Utils_Array::lookupValue($states, 'state_province',
90 CRM_Core_PseudoConstant::stateProvince(), TRUE
91 );
a7488080 92 if (empty($states['state_province_id'])) {
6a488035
TO
93 CRM_Utils_Array::lookupValue($states, 'state_province',
94 CRM_Core_PseudoConstant::stateProvinceAbbreviation(), TRUE
95 );
96 }
9c1bc317 97 $validStates[] = $states['state_province_id'] ?? NULL;
6a488035
TO
98 }
99 $value = implode(CRM_Core_DAO::VALUE_SEPARATOR,
100 $validStates
101 );
102 $type = 'String';
103 }
104 elseif (!$value) {
105 // CRM-3415
106 // using type of timestamp allows us to sneak in a null into db
107 // gross but effective hack
108 $value = NULL;
109 $type = 'Timestamp';
110 }
ec28b24d 111 else {
112 $type = 'String';
113 }
6a488035
TO
114 break;
115
116 case 'Country':
117 $type = 'Integer';
118 if (is_array($value)) {
119 $value = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $value) . CRM_Core_DAO::VALUE_SEPARATOR;
120 $type = 'String';
121 }
ec28b24d 122 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO::VALUE_SEPARATOR)) {
6a488035
TO
123 //fix for multi select country, CRM-3437
124 $mulValues = explode(',', $value);
be2fb01f 125 $validCountries = [];
6a488035 126 foreach ($mulValues as $key => $countryVal) {
be2fb01f 127 $countries = [];
6a488035
TO
128 $countries['country'] = trim($countryVal);
129 CRM_Utils_Array::lookupValue($countries, 'country',
130 CRM_Core_PseudoConstant::country(), TRUE
131 );
a7488080 132 if (empty($countries['country_id'])) {
6a488035
TO
133 CRM_Utils_Array::lookupValue($countries, 'country',
134 CRM_Core_PseudoConstant::countryIsoCode(), TRUE
135 );
136 }
9c1bc317 137 $validCountries[] = $countries['country_id'] ?? NULL;
6a488035
TO
138 }
139 $value = implode(CRM_Core_DAO::VALUE_SEPARATOR,
140 $validCountries
141 );
142 $type = 'String';
143 }
144 elseif (!$value) {
145 // CRM-3415
146 // using type of timestamp allows us to sneak in a null into db
147 // gross but effective hack
148 $value = NULL;
149 $type = 'Timestamp';
150 }
ec28b24d 151 else {
152 $type = 'String';
153 }
6a488035
TO
154 break;
155
156 case 'File':
157 if (!$field['file_id']) {
9d85626b
CW
158 $value = 'null';
159 break;
6a488035
TO
160 }
161
162 // need to add/update civicrm_entity_file
163 $entityFileDAO = new CRM_Core_DAO_EntityFile();
164 $entityFileDAO->file_id = $field['file_id'];
165 $entityFileDAO->find(TRUE);
166
167 $entityFileDAO->entity_table = $field['table_name'];
168 $entityFileDAO->entity_id = $field['entity_id'];
169 $entityFileDAO->file_id = $field['file_id'];
170 $entityFileDAO->save();
6a488035
TO
171 $value = $field['file_id'];
172 $type = 'String';
173 break;
174
175 case 'Date':
176 $value = CRM_Utils_Date::isoToMysql($value);
177 break;
178
179 case 'Int':
180 if (is_numeric($value)) {
181 $type = 'Integer';
182 }
183 else {
184 $type = 'Timestamp';
185 }
186 break;
187
188 case 'ContactReference':
a61d2ab7 189 if ($value == NULL || $value === '' || $value === $VS . $VS) {
6a488035 190 $type = 'Timestamp';
a61d2ab7
CW
191 $value = NULL;
192 }
193 elseif (strpos($value, $VS) !== FALSE) {
194 $type = 'String';
195 // Validate the string contains only integers and value-separators
196 $validChars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, $VS];
197 if (str_replace($validChars, '', $value)) {
198 throw new CRM_Core_Exception('Contact ID must be of type Integer');
199 }
6a488035
TO
200 }
201 else {
202 $type = 'Integer';
203 }
204 break;
205
206 case 'RichTextEditor':
207 $type = 'String';
208 break;
209
210 case 'Boolean':
211 //fix for CRM-3290
212 $value = CRM_Utils_String::strtoboolstr($value);
213 if ($value === FALSE) {
214 $type = 'Timestamp';
215 }
216 break;
217
218 default:
219 break;
220 }
52fb3f74 221 if ($value === 'null') {
fd630ef9 222 // when unsetting a value to null, we don't need to validate the type
223 // https://projectllr.atlassian.net/browse/VGQBMP-20
224 $set[$field['column_name']] = $value;
f931b74c 225 }
226 else {
fd630ef9 227 $set[$field['column_name']] = "%{$count}";
d532d0cf 228 // The second parameter is the type of the db field, which
229 // would be 'String' for a concatenated set of integers.
831545ee 230 // However, the god-forsaken timestamp hack also needs to be kept
231 // if value is NULL.
b99d7139 232 $params[$count] = [$value, ($value && $field['serialize']) ? 'String' : $type];
fd630ef9 233 $count++;
234 }
63b7d442 235
9c1bc317 236 $fieldExtends = $field['extends'] ?? NULL;
63b7d442 237 if (
3d89c1a4
EM
238 CRM_Utils_Array::value('entity_table', $field) === 'civicrm_contact'
239 || $fieldExtends === 'Contact'
240 || $fieldExtends === 'Individual'
241 || $fieldExtends === 'Organization'
242 || $fieldExtends === 'Household'
63b7d442 243 ) {
9c1bc317 244 $paramFieldsExtendContactForEntities[$entityID]['custom_' . CRM_Utils_Array::value('custom_field_id', $field)] = $field['custom_field_id'] ?? NULL;
63b7d442 245 }
6a488035
TO
246 }
247
248 if (!empty($set)) {
be2fb01f 249 $setClause = [];
6a488035 250 foreach ($set as $n => $v) {
4f426b76 251 $setClause[] = "`$n` = $v";
6a488035
TO
252 }
253 $setClause = implode(',', $setClause);
254 if (!$where) {
255 // do this only for insert
256 $set['entity_id'] = "%{$count}";
be2fb01f 257 $params[$count] = [$entityID, 'Integer'];
6a488035
TO
258 $count++;
259
65aae70e 260 $fieldNames = implode(',', CRM_Utils_Type::escapeAll(array_keys($set), 'MysqlColumnNameOrAlias'));
6a488035 261 $fieldValues = implode(',', array_values($set));
353ffa53 262 $query = "$sqlOP ( $fieldNames ) VALUES ( $fieldValues )";
6a488035 263 // for multiple values we dont do on duplicate key update
46fe0a66 264 if (!$isMultiple && $parentOperation !== 'create') {
6a488035
TO
265 $query .= " ON DUPLICATE KEY UPDATE $setClause";
266 }
267 }
268 else {
269 $query = "$sqlOP SET $setClause $where";
270 }
d532d0cf 271 CRM_Core_DAO::executeQuery($query, $params);
6a488035
TO
272
273 CRM_Utils_Hook::custom($hookOP,
445bbeed 274 (int) $firstField['custom_group_id'],
6a488035
TO
275 $entityID,
276 $fields
277 );
278 }
279 }
280 }
63b7d442
AS
281
282 if (!empty($paramFieldsExtendContactForEntities)) {
be2fb01f 283 CRM_Contact_BAO_Contact::updateGreetingsOnTokenFieldChange($paramFieldsExtendContactForEntities, ['contact_id' => $entityID]);
63b7d442 284 }
6a488035
TO
285 }
286
287 /**
fe482240 288 * Given a field return the mysql data type associated with it.
6a488035 289 *
6a0b768e 290 * @param string $type
fd31fa4c
EM
291 * @param int $maxLength
292 *
72b3a70c
CW
293 * @return string
294 * the mysql data store placeholder
6a488035
TO
295 */
296 public static function fieldToSQLType($type, $maxLength = 255) {
297 if (!isset($maxLength) ||
298 !is_numeric($maxLength) ||
299 $maxLength <= 0
300 ) {
301 $maxLength = 255;
302 }
303
304 switch ($type) {
305 case 'String':
306 case 'Link':
307 return "varchar($maxLength)";
308
309 case 'Boolean':
310 return 'tinyint';
311
312 case 'Int':
313 return 'int';
2aa397bc 314
6a488035
TO
315 // the below three are FK's, and have constraints added to them
316
317 case 'ContactReference':
318 case 'StateProvince':
319 case 'Country':
320 case 'File':
321 return 'int unsigned';
322
323 case 'Float':
324 return 'double';
325
326 case 'Money':
327 return 'decimal(20,2)';
328
329 case 'Memo':
330 case 'RichTextEditor':
331 return 'text';
332
333 case 'Date':
334 return 'datetime';
335
336 default:
ac15829d 337 throw new CRM_Core_Exception('Invalid Field Type');
6a488035
TO
338 }
339 }
340
b5c2afd0 341 /**
c490a46a 342 * @param array $params
b5c2afd0 343 * @param $entityTable
100fef9d 344 * @param int $entityID
46fe0a66 345 * @param string $parentOperation Operation being taken on the parent entity.
346 * If we know the parent entity is doing an insert we can skip the
347 * ON DUPLICATE UPDATE - which improves performance and reduces deadlocks.
348 * - edit
349 * - create
b5c2afd0 350 */
46fe0a66 351 public static function store($params, $entityTable, $entityID, $parentOperation = NULL) {
be2fb01f 352 $cvParams = [];
6a488035
TO
353 foreach ($params as $fieldID => $param) {
354 foreach ($param as $index => $customValue) {
be2fb01f 355 $cvParam = [
6a488035
TO
356 'entity_table' => $entityTable,
357 'entity_id' => $entityID,
358 'value' => $customValue['value'],
359 'type' => $customValue['type'],
360 'custom_field_id' => $customValue['custom_field_id'],
361 'custom_group_id' => $customValue['custom_group_id'],
362 'table_name' => $customValue['table_name'],
363 'column_name' => $customValue['column_name'],
b99d7139 364 // is_multiple refers to the custom group, serialize refers to the field.
365 // @todo is_multiple can be null - does that mean anything different from 0?
366 'is_multiple' => $customValue['is_multiple'] ?? CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customValue['custom_group_id'], 'is_multiple'),
367 'serialize' => $customValue['serialize'] ?? (int) CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', $customValue['custom_field_id'], 'serialize'),
6a488035 368 'file_id' => $customValue['file_id'],
be2fb01f 369 ];
6a488035 370
fe482240 371 // Fix Date type to be timestamp, since that is how we store in db.
6a488035
TO
372 if ($cvParam['type'] == 'Date') {
373 $cvParam['type'] = 'Timestamp';
374 }
375
a7488080 376 if (!empty($customValue['id'])) {
6a488035
TO
377 $cvParam['id'] = $customValue['id'];
378 }
379 if (!array_key_exists($customValue['table_name'], $cvParams)) {
be2fb01f 380 $cvParams[$customValue['table_name']] = [];
6a488035
TO
381 }
382
383 if (!array_key_exists($index, $cvParams[$customValue['table_name']])) {
be2fb01f 384 $cvParams[$customValue['table_name']][$index] = [];
6a488035
TO
385 }
386
387 $cvParams[$customValue['table_name']][$index][] = $cvParam;
388 }
389 }
390 if (!empty($cvParams)) {
46fe0a66 391 self::create($cvParams, $parentOperation);
6a488035
TO
392 }
393 }
394
b5c2afd0 395 /**
fe482240
EM
396 * Post process function.
397 *
c490a46a 398 * @param array $params
b5c2afd0 399 * @param $entityTable
100fef9d 400 * @param int $entityID
b5c2afd0 401 * @param $customFieldExtends
11201e17 402 * @param $parentOperation
b5c2afd0 403 */
11201e17 404 public static function postProcess(&$params, $entityTable, $entityID, $customFieldExtends, $parentOperation = NULL) {
6a488035 405 $customData = CRM_Core_BAO_CustomField::postProcess($params,
6a488035
TO
406 $entityID,
407 $customFieldExtends
408 );
409
410 if (!empty($customData)) {
11201e17 411 self::store($customData, $entityTable, $entityID, $parentOperation);
6a488035
TO
412 }
413 }
414
415 /**
416 * Return an array of all custom values associated with an entity.
417 *
6a0b768e
TO
418 * @param int $entityID
419 * Identification number of the entity.
420 * @param string $entityType
421 * Type of entity that the entityID corresponds to, specified.
6a488035
TO
422 * as a string with format "'<EntityName>'". Comma separated
423 * list may be used to specify OR matches. Allowable values
424 * are enumerated types in civicrm_custom_group.extends field.
425 * Optional. Default value assumes entityID references a
426 * contact entity.
6a0b768e
TO
427 * @param array $fieldIDs
428 * Optional list of fieldIDs that we want to retrieve. If this.
6a488035
TO
429 * is set the entityType is ignored
430 *
77b97be7 431 * @param bool $formatMultiRecordField
c693f065 432 * @param array $DTparams - CRM-17810 dataTable params for the multiValued custom fields.
77b97be7 433 *
a6c01b45
CW
434 * @return array
435 * Array of custom values for the entity with key=>value
6a488035
TO
436 * pairs specified as civicrm_custom_field.id => custom value.
437 * Empty array if no custom values found.
ac15829d 438 * @throws CRM_Core_Exception
6a488035 439 */
c693f065 440 public static function &getEntityValues($entityID, $entityType = NULL, $fieldIDs = NULL, $formatMultiRecordField = FALSE, $DTparams = NULL) {
6a488035
TO
441 if (!$entityID) {
442 // adding this here since an empty contact id could have serious repurcussions
443 // like looping forever
ac15829d 444 throw new CRM_Core_Exception('Please file an issue with the backtrace');
6a488035
TO
445 return NULL;
446 }
447
be2fb01f 448 $cond = [];
6a488035
TO
449 if ($entityType) {
450 $cond[] = "cg.extends IN ( '$entityType' )";
451 }
452 if ($fieldIDs &&
453 is_array($fieldIDs)
454 ) {
455 $fieldIDList = implode(',', $fieldIDs);
456 $cond[] = "cf.id IN ( $fieldIDList )";
457 }
458 if (empty($cond)) {
459 $cond[] = "cg.extends IN ( 'Contact', 'Individual', 'Household', 'Organization' )";
460 }
461 $cond = implode(' AND ', $cond);
462
e87c8fb7 463 $limit = $orderBy = '';
c693f065 464 if (!empty($DTparams['rowCount']) && $DTparams['rowCount'] > 0) {
d51e02d3 465 $limit = " LIMIT " . CRM_Utils_Type::escape($DTparams['offset'], 'Integer') . ", " . CRM_Utils_Type::escape($DTparams['rowCount'], 'Integer');
c693f065 466 }
c693f065 467 if (!empty($DTparams['sort'])) {
468 $orderBy = ' ORDER BY ' . CRM_Utils_Type::escape($DTparams['sort'], 'String');
469 }
470
fe482240 471 // First find all the fields that extend this type of entity.
6a488035
TO
472 $query = "
473SELECT cg.table_name,
474 cg.id as groupID,
475 cg.is_multiple,
476 cf.column_name,
34f51a07
N
477 cf.id as fieldID,
478 cf.data_type as fieldDataType
6a488035
TO
479FROM civicrm_custom_group cg,
480 civicrm_custom_field cf
481WHERE cf.custom_group_id = cg.id
482AND cg.is_active = 1
483AND cf.is_active = 1
484AND $cond
485";
486 $dao = CRM_Core_DAO::executeQuery($query);
487
be2fb01f 488 $select = $fields = $isMultiple = [];
6a488035
TO
489
490 while ($dao->fetch()) {
491 if (!array_key_exists($dao->table_name, $select)) {
be2fb01f
CW
492 $fields[$dao->table_name] = [];
493 $select[$dao->table_name] = [];
6a488035
TO
494 }
495 $fields[$dao->table_name][] = $dao->fieldID;
496 $select[$dao->table_name][] = "{$dao->column_name} AS custom_{$dao->fieldID}";
63d76404 497 $isMultiple[$dao->table_name] = (bool) $dao->is_multiple;
77b97be7 498 $file[$dao->table_name][$dao->fieldID] = $dao->fieldDataType;
6a488035
TO
499 }
500
be2fb01f 501 $result = $sortedResult = [];
6a488035 502 foreach ($select as $tableName => $clauses) {
1c66bdc7 503 if (!empty($DTparams['sort'])) {
504 $query = CRM_Core_DAO::executeQuery("SELECT id FROM {$tableName} WHERE entity_id = {$entityID}");
505 $count = 1;
506 while ($query->fetch()) {
507 $sortedResult["{$query->id}"] = $count;
508 $count++;
509 }
510 }
511
c693f065 512 $query = "SELECT SQL_CALC_FOUND_ROWS id, " . implode(', ', $clauses) . " FROM $tableName WHERE entity_id = $entityID {$orderBy} {$limit}";
6a488035 513 $dao = CRM_Core_DAO::executeQuery($query);
e87c8fb7 514 if (!empty($DTparams)) {
515 $result['count'] = CRM_Core_DAO::singleValueQuery('SELECT FOUND_ROWS()');
516 }
6a488035
TO
517 while ($dao->fetch()) {
518 foreach ($fields[$tableName] as $fieldID) {
519 $fieldName = "custom_{$fieldID}";
520 if ($isMultiple[$tableName]) {
521 if ($formatMultiRecordField) {
d8f34a6e 522 $result["{$dao->id}"]["{$fieldID}"] = $dao->$fieldName;
fe482240
EM
523 }
524 else {
6a488035
TO
525 $result["{$fieldID}_{$dao->id}"] = $dao->$fieldName;
526 }
527 }
528 else {
d8f34a6e 529 $result[$fieldID] = $dao->$fieldName;
6a488035
TO
530 }
531 }
532 }
533 }
e525d6af 534 if (!empty($sortedResult)) {
535 $result['sortedResult'] = $sortedResult;
1c66bdc7 536 }
6a488035
TO
537 return $result;
538 }
539
540 /**
100fef9d 541 * Take in an array of entityID, custom_XXX => value
6a488035
TO
542 * and set the value in the appropriate table. Should also be able
543 * to set the value to null. Follows api parameter/return conventions
544 *
545 * @array $params
546 *
c490a46a 547 * @param array $params
2a6da8d7
EM
548 *
549 * @throws Exception
6a488035 550 * @return array
6a488035 551 */
00be9182 552 public static function setValues(&$params) {
d1b0ffad
CW
553 // For legacy reasons, accept this param in either format
554 if (empty($params['entityID']) && !empty($params['entity_id'])) {
555 $params['entityID'] = $params['entity_id'];
556 }
6a488035 557
d1b0ffad 558 if (!isset($params['entityID']) || !CRM_Utils_Type::validate($params['entityID'], 'Integer', FALSE)) {
e31fc637 559 throw new CRM_Core_Exception(ts('entity_id needs to be set and of type Integer'));
6a488035
TO
560 }
561
562 // first collect all the id/value pairs. The format is:
563 // custom_X => value or custom_X_VALUEID => value (for multiple values), VALUEID == -1, -2 etc for new insertions
be2fb01f 564 $fieldValues = [];
6a488035
TO
565 foreach ($params as $n => $v) {
566 if ($customFieldInfo = CRM_Core_BAO_CustomField::getKeyID($n, TRUE)) {
567 $fieldID = (int ) $customFieldInfo[0];
568 if (CRM_Utils_Type::escape($fieldID, 'Integer', FALSE) === NULL) {
e31fc637 569 throw new CRM_Core_Exception(ts('field ID needs to be of type Integer for index %1',
be2fb01f 570 [1 => $fieldID]
353ffa53 571 ));
6a488035
TO
572 }
573 if (!array_key_exists($fieldID, $fieldValues)) {
be2fb01f 574 $fieldValues[$fieldID] = [];
6a488035
TO
575 }
576 $id = -1;
577 if ($customFieldInfo[1]) {
578 $id = (int ) $customFieldInfo[1];
579 }
be2fb01f 580 $fieldValues[$fieldID][] = [
6a488035
TO
581 'value' => $v,
582 'id' => $id,
be2fb01f 583 ];
6a488035
TO
584 }
585 }
586
587 $fieldIDList = implode(',', array_keys($fieldValues));
588
589 // format it so that we can just use create
590 $sql = "
591SELECT cg.table_name as table_name ,
592 cg.id as cg_id ,
593 cg.is_multiple as is_multiple,
63b7d442 594 cg.extends as extends,
6a488035
TO
595 cf.column_name as column_name,
596 cf.id as cf_id ,
3766bd36 597 cf.html_type as html_type ,
b99d7139 598 cf.data_type as data_type ,
599 cf.serialize as serialize
6a488035
TO
600FROM civicrm_custom_group cg,
601 civicrm_custom_field cf
602WHERE cf.custom_group_id = cg.id
603AND cf.id IN ( $fieldIDList )
604";
605
606 $dao = CRM_Core_DAO::executeQuery($sql);
be2fb01f 607 $cvParams = [];
6a488035
TO
608
609 while ($dao->fetch()) {
610 $dataType = $dao->data_type == 'Date' ? 'Timestamp' : $dao->data_type;
611 foreach ($fieldValues[$dao->cf_id] as $fieldValue) {
3766bd36
CW
612 // Serialize array values
613 if (is_array($fieldValue['value']) && CRM_Core_BAO_CustomField::isSerialized($dao)) {
614 $fieldValue['value'] = CRM_Utils_Array::implodePadded($fieldValue['value']);
615 }
6a488035
TO
616 // Format null values correctly
617 if ($fieldValue['value'] === NULL || $fieldValue['value'] === '') {
618 switch ($dataType) {
619 case 'String':
620 case 'Int':
621 case 'Link':
622 case 'Boolean':
623 $fieldValue['value'] = '';
624 break;
625
626 case 'Timestamp':
627 $fieldValue['value'] = NULL;
628 break;
629
630 case 'StateProvince':
631 case 'Country':
632 case 'Money':
633 case 'Float':
2aa397bc 634 $fieldValue['value'] = (int) 0;
6a488035
TO
635 break;
636 }
637 }
638 // Ensure that value is of the right data type
639 elseif (CRM_Utils_Type::escape($fieldValue['value'], $dataType, FALSE) === NULL) {
e31fc637 640 throw new CRM_Core_Exception(ts('value: %1 is not of the right field data type: %2',
be2fb01f 641 [
353ffa53
TO
642 1 => $fieldValue['value'],
643 2 => $dao->data_type,
be2fb01f 644 ]
353ffa53 645 ));
6a488035
TO
646 }
647
be2fb01f 648 $cvParam = [
6a488035
TO
649 'entity_id' => $params['entityID'],
650 'value' => $fieldValue['value'],
651 'type' => $dataType,
652 'custom_field_id' => $dao->cf_id,
653 'custom_group_id' => $dao->cg_id,
654 'table_name' => $dao->table_name,
655 'column_name' => $dao->column_name,
656 'is_multiple' => $dao->is_multiple,
b99d7139 657 'serialize' => $dao->serialize,
63b7d442 658 'extends' => $dao->extends,
be2fb01f 659 ];
6a488035 660
5013100c 661 if (!empty($params['id'])) {
662 $cvParam['id'] = $params['id'];
663 }
664
e7dcccf0
CW
665 if ($cvParam['type'] == 'File') {
666 $cvParam['file_id'] = $fieldValue['value'];
667 }
668
6a488035 669 if (!array_key_exists($dao->table_name, $cvParams)) {
be2fb01f 670 $cvParams[$dao->table_name] = [];
6a488035
TO
671 }
672
673 if (!array_key_exists($fieldValue['id'], $cvParams[$dao->table_name])) {
be2fb01f 674 $cvParams[$dao->table_name][$fieldValue['id']] = [];
6a488035
TO
675 }
676
677 if ($fieldValue['id'] > 0) {
678 $cvParam['id'] = $fieldValue['id'];
679 }
680 $cvParams[$dao->table_name][$fieldValue['id']][] = $cvParam;
681 }
682 }
683
684 if (!empty($cvParams)) {
685 self::create($cvParams);
be2fb01f 686 return ['is_error' => 0, 'result' => 1];
6a488035
TO
687 }
688
e31fc637 689 throw new CRM_Core_Exception(ts('Unknown error'));
6a488035
TO
690 }
691
692 /**
100fef9d 693 * Take in an array of entityID, custom_ID
6a488035
TO
694 * and gets the value from the appropriate table.
695 *
696 * To get the values of custom fields with IDs 13 and 43 for contact ID 1327, use:
697 * $params = array( 'entityID' => 1327, 'custom_13' => 1, 'custom_43' => 1 );
698 *
b44e3f84 699 * Entity Type will be inferred by the custom fields you request
6a488035
TO
700 * Specify $params['entityType'] if you do not supply any custom fields to return
701 * and entity type is other than Contact
702 *
703 * @array $params
704 *
c490a46a 705 * @param array $params
2a6da8d7
EM
706 *
707 * @throws Exception
6a488035 708 * @return array
6a488035 709 */
00be9182 710 public static function &getValues(&$params) {
6a488035
TO
711 if (empty($params)) {
712 return NULL;
713 }
714 if (!isset($params['entityID']) ||
715 CRM_Utils_Type::escape($params['entityID'],
716 'Integer', FALSE
717 ) === NULL
718 ) {
719 return CRM_Core_Error::createAPIError(ts('entityID needs to be set and of type Integer'));
720 }
721
722 // first collect all the ids. The format is:
723 // custom_ID
be2fb01f 724 $fieldIDs = [];
6a488035
TO
725 foreach ($params as $n => $v) {
726 $key = $idx = NULL;
727 if (substr($n, 0, 7) == 'custom_') {
728 $idx = substr($n, 7);
729 if (CRM_Utils_Type::escape($idx, 'Integer', FALSE) === NULL) {
730 return CRM_Core_Error::createAPIError(ts('field ID needs to be of type Integer for index %1',
be2fb01f 731 [1 => $idx]
353ffa53 732 ));
6a488035
TO
733 }
734 $fieldIDs[] = (int ) $idx;
735 }
736 }
737
be2fb01f 738 $default = ['Contact', 'Individual', 'Household', 'Organization'];
6a488035
TO
739 if (!($type = CRM_Utils_Array::value('entityType', $params)) ||
740 in_array($params['entityType'], $default)
741 ) {
742 $type = NULL;
743 }
744 else {
745 $entities = CRM_Core_SelectValues::customGroupExtends();
746 if (!array_key_exists($type, $entities)) {
747 if (in_array($type, $entities)) {
748 $type = $entities[$type];
749 if (in_array($type, $default)) {
750 $type = NULL;
751 }
752 }
753 else {
754 return CRM_Core_Error::createAPIError(ts('Invalid entity type') . ': "' . $type . '"');
755 }
756 }
757 }
758
759 $values = self::getEntityValues($params['entityID'],
760 $type,
761 $fieldIDs
762 );
763 if (empty($values)) {
764 // note that this behaviour is undesirable from an API point of view - it should return an empty array
765 // since this is also called by the merger code & not sure the consequences of changing
766 // are just handling undoing this in the api layer. ie. converting the error back into a success
be2fb01f 767 $result = [
6a488035
TO
768 'is_error' => 1,
769 'error_message' => 'No values found for the specified entity ID and custom field(s).',
be2fb01f 770 ];
6a488035
TO
771 return $result;
772 }
773 else {
be2fb01f 774 $result = [
6a488035
TO
775 'is_error' => 0,
776 'entityID' => $params['entityID'],
be2fb01f 777 ];
6a488035
TO
778 foreach ($values as $id => $value) {
779 $result["custom_{$id}"] = $value;
780 }
781 return $result;
782 }
783 }
96025800 784
6a488035 785}