Merge pull request #23455 from eileenmcnaughton/import_validate_mum
[civicrm-core.git] / CRM / Import / ImportProcessor.php
CommitLineData
0b0285b1 1<?php
2
96907427
EM
3use Civi\Api4\Mapping;
4use Civi\Api4\MappingField;
5
0b0285b1 6/**
7 * Class CRM_Import_ImportProcessor.
8 *
9 * Import processor class. This is intended to provide a sanitising wrapper around
10 * the form-oriented import classes. In particular it is intended to provide a clear translation
11 * between the saved mapping field format and the quick form & parser formats.
12 *
13 * In the first instance this is only being used in unit tests but the intent is to migrate
14 * to it on a trajectory similar to the ExportProcessor so it is not in the tests.
15 */
16class CRM_Import_ImportProcessor {
17
18 /**
19 * An array of fields in the format used in the table civicrm_mapping_field.
20 *
21 * @var array
22 */
23 protected $mappingFields = [];
24
92bde46a 25 /**
26 * @var array
27 */
28 protected $metadata = [];
29
52bd01f5
EM
30 /**
31 * Id of the created user job.
32 *
33 * @var int
34 */
35 protected $userJobID;
36
37 /**
38 * @return int
39 */
40 public function getUserJobID(): int {
41 return $this->userJobID;
42 }
43
44 /**
45 * @param int $userJobID
46 */
47 public function setUserJobID(int $userJobID): void {
48 $this->userJobID = $userJobID;
49 }
50
92bde46a 51 /**
52 * Metadata keyed by field title.
53 *
54 * @var array
55 */
56 protected $metadataByTitle = [];
57
0b0285b1 58 /**
59 * Get contact type being imported.
60 *
61 * @var string
62 */
63 protected $contactType;
64
92bde46a 65 /**
66 * Get contact sub type being imported.
67 *
68 * @var string
69 */
70 protected $contactSubType;
71
72 /**
73 * Array of valid relationships for the contact type & subtype.
74 *
75 * @var array
76 */
77 protected $validRelationships = [];
78
79 /**
80 * Name of the form.
81 *
82 * Used for js for quick form.
83 *
84 * @var string
85 */
86 protected $formName;
87
88 /**
89 * @return string
90 */
91 public function getFormName(): string {
92 return $this->formName;
93 }
94
95 /**
96 * @param string $formName
97 */
98 public function setFormName(string $formName) {
99 $this->formName = $formName;
100 }
101
102 /**
103 * @return array
104 */
105 public function getValidRelationships(): array {
106 if (!isset($this->validRelationships[$this->getContactType() . '_' . $this->getContactSubType()])) {
107 //Relationship importables
108 $relations = CRM_Contact_BAO_Relationship::getContactRelationshipType(
109 NULL, NULL, NULL, $this->getContactType(),
110 FALSE, 'label', TRUE, $this->getContactSubType()
111 );
112 asort($relations);
113 $this->setValidRelationships($relations);
114 }
115 return $this->validRelationships[$this->getContactType() . '_' . $this->getContactSubType()];
116 }
117
118 /**
119 * @param array $validRelationships
120 */
121 public function setValidRelationships(array $validRelationships) {
122 $this->validRelationships[$this->getContactType() . '_' . $this->getContactSubType()] = $validRelationships;
123 }
124
125 /**
126 * Get contact subtype for import.
127 *
128 * @return string
129 */
130 public function getContactSubType(): string {
b1498fda 131 return $this->contactSubType ?? '';
92bde46a 132 }
133
134 /**
135 * Set contact subtype for import.
136 *
137 * @param string $contactSubType
138 */
a98c8ecb 139 public function setContactSubType($contactSubType) {
140 $this->contactSubType = (string) $contactSubType;
92bde46a 141 }
142
143 /**
144 * Saved Mapping ID.
145 *
146 * @var int
147 */
148 protected $mappingID;
149
150 /**
151 * @return array
152 */
153 public function getMetadata(): array {
154 return $this->metadata;
155 }
156
157 /**
158 * Setting for metadata.
159 *
160 * We wrangle the label for custom fields to include the label since the
161 * metadata trait presents it in a more 'pure' form but the label is appended for importing.
162 *
163 * @param array $metadata
164 *
165 * @throws \CiviCRM_API3_Exception
166 */
167 public function setMetadata(array $metadata) {
168 $fieldDetails = civicrm_api3('CustomField', 'get', [
169 'return' => ['custom_group_id.title'],
170 'options' => ['limit' => 0],
171 ])['values'];
172 foreach ($metadata as $index => $field) {
173 if (!empty($field['custom_field_id'])) {
174 // The 'label' format for import is custom group title :: custom name title
175 $metadata[$index]['name'] = $index;
176 $metadata[$index]['title'] .= ' :: ' . $fieldDetails[$field['custom_field_id']]['custom_group_id.title'];
177 }
178 }
179 $this->metadata = $metadata;
180 }
181
182 /**
183 * @return int
184 */
185 public function getMappingID(): int {
186 return $this->mappingID;
187 }
188
189 /**
190 * @param int $mappingID
191 */
192 public function setMappingID(int $mappingID) {
193 $this->mappingID = $mappingID;
194 }
195
0b0285b1 196 /**
a7b9cf38 197 * Get the contact type for the import.
198 *
0b0285b1 199 * @return string
200 */
201 public function getContactType(): string {
202 return $this->contactType;
203 }
204
205 /**
206 * @param string $contactType
207 */
208 public function setContactType(string $contactType) {
209 $this->contactType = $contactType;
210 }
211
212 /**
92bde46a 213 * Set the contact type according to the constant.
214 *
215 * @param int $contactTypeKey
216 */
217 public function setContactTypeByConstant($contactTypeKey) {
218 $constantTypeMap = [
219 CRM_Import_Parser::CONTACT_INDIVIDUAL => 'Individual',
220 CRM_Import_Parser::CONTACT_HOUSEHOLD => 'Household',
221 CRM_Import_Parser::CONTACT_ORGANIZATION => 'Organization',
222 ];
223 $this->contactType = $constantTypeMap[$contactTypeKey];
224 }
225
226 /**
227 * Get Mapping Fields.
228 *
0b0285b1 229 * @return array
92bde46a 230 *
231 * @throws \CiviCRM_API3_Exception
0b0285b1 232 */
233 public function getMappingFields(): array {
92bde46a 234 if (empty($this->mappingFields) && !empty($this->getMappingID())) {
235 $this->loadSavedMapping();
236 }
0b0285b1 237 return $this->mappingFields;
238 }
239
240 /**
a7b9cf38 241 * Set mapping fields.
242 *
243 * We do a little cleanup here too.
244 *
245 * We ensure that column numbers are set and that the fields are ordered by them.
246 *
247 * This would mean the fields could be loaded unsorted.
248 *
0b0285b1 249 * @param array $mappingFields
250 */
251 public function setMappingFields(array $mappingFields) {
a7b9cf38 252 $i = 0;
253 foreach ($mappingFields as &$mappingField) {
254 if (!isset($mappingField['column_number'])) {
255 $mappingField['column_number'] = $i;
256 }
257 if ($mappingField['column_number'] > $i) {
258 $i = $mappingField['column_number'];
259 }
260 $i++;
261 }
92bde46a 262 $this->mappingFields = $this->rekeyBySortedColumnNumbers($mappingFields);
0b0285b1 263 }
264
265 /**
266 * Get the names of the mapped fields.
92bde46a 267 *
268 * @throws \CiviCRM_API3_Exception
0b0285b1 269 */
270 public function getFieldNames() {
271 return CRM_Utils_Array::collect('name', $this->getMappingFields());
272 }
273
92bde46a 274 /**
275 * Get the field name for the given column.
276 *
277 * @param int $columnNumber
278 *
279 * @return string
280 * @throws \CiviCRM_API3_Exception
281 */
282 public function getFieldName($columnNumber) {
283 return $this->getFieldNames()[$columnNumber];
284 }
285
286 /**
287 * Get the field name for the given column.
288 *
289 * @param int $columnNumber
290 *
291 * @return string
292 * @throws \CiviCRM_API3_Exception
293 */
294 public function getRelationshipKey($columnNumber) {
295 $field = $this->getMappingFields()[$columnNumber];
296 return empty($field['relationship_type_id']) ? NULL : $field['relationship_type_id'] . '_' . $field['relationship_direction'];
297 }
298
299 /**
300 * Get relationship key only if it is valid.
301 *
302 * @param int $columnNumber
303 *
304 * @return string|null
305 *
306 * @throws \CiviCRM_API3_Exception
307 */
308 public function getValidRelationshipKey($columnNumber) {
309 $key = $this->getRelationshipKey($columnNumber);
310 return $this->isValidRelationshipKey($key) ? $key : NULL;
311 }
312
313 /**
314 * Get the IM Provider ID.
315 *
316 * @param int $columnNumber
317 *
318 * @return int
319 *
320 * @throws \CiviCRM_API3_Exception
321 */
322 public function getIMProviderID($columnNumber) {
323 return $this->getMappingFields()[$columnNumber]['im_provider_id'] ?? NULL;
324 }
325
326 /**
327 * Get the Phone Type
328 *
329 * @param int $columnNumber
330 *
331 * @return int
332 *
333 * @throws \CiviCRM_API3_Exception
334 */
335 public function getPhoneTypeID($columnNumber) {
336 return $this->getMappingFields()[$columnNumber]['phone_type_id'] ?? NULL;
337 }
338
339 /**
340 * Get the Website Type
341 *
342 * @param int $columnNumber
343 *
344 * @return int
345 *
346 * @throws \CiviCRM_API3_Exception
347 */
348 public function getWebsiteTypeID($columnNumber) {
349 return $this->getMappingFields()[$columnNumber]['website_type_id'] ?? NULL;
350 }
351
352 /**
353 * Get the Location Type
354 *
355 * Returning 0 rather than null is historical.
356 *
357 * @param int $columnNumber
358 *
359 * @return int
360 *
361 * @throws \CiviCRM_API3_Exception
362 */
363 public function getLocationTypeID($columnNumber) {
364 return $this->getMappingFields()[$columnNumber]['location_type_id'] ?? 0;
365 }
366
367 /**
368 * Get the IM or Phone type.
369 *
370 * We have a field that would be the 'relevant' type - which could be either.
371 *
372 * @param int $columnNumber
373 *
374 * @return int
375 *
376 * @throws \CiviCRM_API3_Exception
377 */
378 public function getPhoneOrIMTypeID($columnNumber) {
379 return $this->getIMProviderID($columnNumber) ?? $this->getPhoneTypeID($columnNumber);
380 }
381
0b0285b1 382 /**
383 * Get the location types of the mapped fields.
92bde46a 384 *
385 * @throws \CiviCRM_API3_Exception
0b0285b1 386 */
387 public function getFieldLocationTypes() {
388 return CRM_Utils_Array::collect('location_type_id', $this->getMappingFields());
389 }
390
391 /**
392 * Get the phone types of the mapped fields.
92bde46a 393 *
394 * @throws \CiviCRM_API3_Exception
0b0285b1 395 */
396 public function getFieldPhoneTypes() {
397 return CRM_Utils_Array::collect('phone_type_id', $this->getMappingFields());
398 }
399
400 /**
401 * Get the names of the im_provider fields.
92bde46a 402 *
403 * @throws \CiviCRM_API3_Exception
0b0285b1 404 */
405 public function getFieldIMProviderTypes() {
406 return CRM_Utils_Array::collect('im_provider_id', $this->getMappingFields());
407 }
408
409 /**
410 * Get the names of the website fields.
92bde46a 411 *
412 * @throws \CiviCRM_API3_Exception
0b0285b1 413 */
414 public function getFieldWebsiteTypes() {
415 return CRM_Utils_Array::collect('im_provider_id', $this->getMappingFields());
416 }
417
418 /**
419 * Get an instance of the importer object.
420 *
421 * @return CRM_Contact_Import_Parser_Contact
92bde46a 422 *
423 * @throws \CiviCRM_API3_Exception
0b0285b1 424 */
425 public function getImporterObject() {
426 $importer = new CRM_Contact_Import_Parser_Contact(
427 $this->getFieldNames(),
428 $this->getFieldLocationTypes(),
429 $this->getFieldPhoneTypes(),
430 $this->getFieldIMProviderTypes(),
431 // @todo - figure out related mappings.
432 // $mapperRelated = [], $mapperRelatedContactType = [], $mapperRelatedContactDetails = [], $mapperRelatedContactLocType = [], $mapperRelatedContactPhoneType = [], $mapperRelatedContactImProvider = [],
433 [],
434 [],
435 [],
436 [],
437 [],
438 [],
439 $this->getFieldWebsiteTypes()
440 // $mapperRelatedContactWebsiteType = []
441 );
52bd01f5 442 $importer->setUserJobID($this->getUserJobID());
0b0285b1 443 $importer->init();
0b0285b1 444 return $importer;
445 }
446
92bde46a 447 /**
448 * Load the mapping from the datbase into the format that would be received from the UI.
449 *
450 * @throws \CiviCRM_API3_Exception
451 */
452 protected function loadSavedMapping() {
96907427
EM
453 $fields = civicrm_api3('MappingField', 'get', [
454 'mapping_id' => $this->getMappingID(),
455 'options' => ['limit' => 0],
456 ])['values'];
457 foreach ($fields as $index => $field) {
458 $fieldSpec = $this->getMetadata()[$fields[$index]['name']];
459 $fields[$index]['label'] = $fieldSpec['title'];
460 if (empty($field['location_type_id']) && !empty($fieldSpec['hasLocationType'])) {
461 $fields[$index]['location_type_id'] = 'Primary';
462 }
463 }
464 $this->mappingFields = $this->rekeyBySortedColumnNumbers($fields);
465 }
466
467 /**
468 * Load the mapping from the database into the pre-5.50 format.
469 *
470 * This is preserved as a copy the upgrade script can use - since the
471 * upgrade allows the other to be 'fixed'.
472 *
473 * @throws \CiviCRM_API3_Exception
474 */
475 protected function legacyLoadSavedMapping() {
92bde46a 476 $fields = civicrm_api3('MappingField', 'get', [
477 'mapping_id' => $this->getMappingID(),
478 'options' => ['limit' => 0],
479 ])['values'];
480 foreach ($fields as $index => $field) {
481 // Fix up the fact that for lost reasons we save by label not name.
482 $fields[$index]['label'] = $field['name'];
483 if (empty($field['relationship_type_id'])) {
484 $fields[$index]['name'] = $this->getNameFromLabel($field['name']);
485 }
486 else {
487 // Honour legacy chaos factor.
471ddc82 488 if ($field['name'] === ts('- do not import -')) {
489 // This is why we save names not labels people....
490 $field['name'] = 'do_not_import';
491 }
92bde46a 492 $fields[$index]['name'] = strtolower(str_replace(" ", "_", $field['name']));
493 // fix for edge cases, CRM-4954
494 if ($fields[$index]['name'] === 'image_url') {
495 $fields[$index]['name'] = str_replace('url', 'URL', $fields[$index]['name']);
496 }
497 }
498 $fieldSpec = $this->getMetadata()[$fields[$index]['name']];
499 if (empty($field['location_type_id']) && !empty($fieldSpec['hasLocationType'])) {
500 $fields[$index]['location_type_id'] = 'Primary';
501 }
502 }
503 $this->mappingFields = $this->rekeyBySortedColumnNumbers($fields);
504 }
505
506 /**
507 * Get the titles from metadata.
508 */
509 public function getMetadataTitles() {
510 if (empty($this->metadataByTitle)) {
511 $this->metadataByTitle = CRM_Utils_Array::collect('title', $this->getMetadata());
512 }
513 return $this->metadataByTitle;
514 }
515
516 /**
517 * Rekey the array by the column_number.
518 *
519 * @param array $mappingFields
520 *
521 * @return array
522 */
523 protected function rekeyBySortedColumnNumbers(array $mappingFields) {
524 $this->mappingFields = CRM_Utils_Array::rekey($mappingFields, 'column_number');
525 ksort($this->mappingFields);
cd41fa5b 526 return $this->mappingFields;
92bde46a 527 }
528
529 /**
530 * Get the field name from the label.
531 *
532 * @param string $label
533 *
534 * @return string
535 */
536 protected function getNameFromLabel($label) {
537 $titleMap = array_flip($this->getMetadataTitles());
4bef4a25 538 $label = str_replace(' (match to contact)', '', $label);
92bde46a 539 return $titleMap[$label] ?? '';
540 }
541
542 /**
543 * Validate the key against the relationships available for the contatct type & subtype.
544 *
545 * @param string $key
546 *
547 * @return bool
548 */
549 protected function isValidRelationshipKey($key) {
f7dbf5d9 550 return !empty($this->getValidRelationships()[$key]);
92bde46a 551 }
552
3c238265 553 /**
554 * Get the relevant js for quickform.
555 *
556 * @param int $column
557 *
558 * @return string
559 * @throws \CiviCRM_API3_Exception
560 */
561 public function getQuickFormJSForField($column) {
562 $columnNumbersToHide = [];
471ddc82 563 if ($this->getFieldName($column) === 'do_not_import') {
564 $columnNumbersToHide = [1, 2, 3];
565 }
566 elseif ($this->getRelationshipKey($column)) {
a9f13f01 567 if (!$this->getWebsiteTypeID($column) && !$this->getLocationTypeID($column)) {
568 $columnNumbersToHide[] = 2;
569 }
570 if (!$this->getFieldName($column)) {
571 $columnNumbersToHide[] = 1;
572 }
573 if (!$this->getPhoneOrIMTypeID($column)) {
574 $columnNumbersToHide[] = 3;
575 }
3c238265 576 }
a9f13f01 577 else {
578 if (!$this->getLocationTypeID($column) && !$this->getWebsiteTypeID($column)) {
579 $columnNumbersToHide[] = 1;
580 }
581 if (!$this->getPhoneOrIMTypeID($column)) {
582 $columnNumbersToHide[] = 2;
583 }
584 $columnNumbersToHide[] = 3;
3c238265 585 }
3c238265 586
587 $jsClauses = [];
588 foreach ($columnNumbersToHide as $columnNumber) {
589 $jsClauses[] = $this->getFormName() . "['mapper[$column][" . $columnNumber . "]'].style.display = 'none';";
590 }
a9f13f01 591 return empty($jsClauses) ? '' : implode("\n", $jsClauses) . "\n";
3c238265 592 }
593
df90a4dd 594 /**
595 * Get the defaults for the column from the saved mapping.
596 *
597 * @param int $column
598 *
599 * @return array
600 * @throws \CiviCRM_API3_Exception
601 */
602 public function getSavedQuickformDefaultsForColumn($column) {
471ddc82 603 if ($this->getFieldName($column) === 'do_not_import') {
92e44801 604 return [];
605 }
e013bf20 606 if ($this->getValidRelationshipKey($column)) {
607 if ($this->getWebsiteTypeID($column)) {
608 return [$this->getValidRelationshipKey($column), $this->getFieldName($column), $this->getWebsiteTypeID($column)];
609 }
610 return [$this->getValidRelationshipKey($column), $this->getFieldName($column), $this->getLocationTypeID($column), $this->getPhoneOrIMTypeID($column)];
611 }
df90a4dd 612 if ($this->getWebsiteTypeID($column)) {
613 return [$this->getFieldName($column), $this->getWebsiteTypeID($column)];
614 }
615 return [(string) $this->getFieldName($column), $this->getLocationTypeID($column), $this->getPhoneOrIMTypeID($column)];
616 }
617
96907427
EM
618 /**
619 * This exists for use in the FiveFifty Upgrade
620 *
621 * @throws \API_Exception|\CiviCRM_API3_Exception
622 */
623 public static function convertSavedFields(): void {
624 $mappings = Mapping::get(FALSE)
625 ->setSelect(['id', 'contact_type'])
626 ->addWhere('mapping_type_id:name', '=', 'Import Contact')
627 ->execute();
628
629 foreach ($mappings as $mapping) {
630 $processor = new CRM_Import_ImportProcessor();
631 $processor->setMappingID($mapping['id']);
632 $processor->setMetadata(CRM_Contact_BAO_Contact::importableFields('All'));
633 $processor->legacyLoadSavedMapping();;
634 foreach ($processor->getMappingFields() as $field) {
635 // The if is mostly precautionary against running this more than once
636 // - which is common in dev if not live...
637 if ($field['name']) {
638 MappingField::update(FALSE)
639 ->setValues(['name' => $field['name']])
640 ->addWhere('id', '=', $field['id'])
641 ->execute();
642 }
643 }
644 }
645 }
646
0b0285b1 647}