4 use Civi\Api4\MappingField
;
7 * Class CRM_Import_ImportProcessor.
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.
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.
16 class CRM_Import_ImportProcessor
{
19 * An array of fields in the format used in the table civicrm_mapping_field.
23 protected $mappingFields = [];
28 protected $metadata = [];
31 * Id of the created user job.
40 public function getUserJobID(): int {
41 return $this->userJobID
;
45 * @param int $userJobID
47 public function setUserJobID(int $userJobID): void
{
48 $this->userJobID
= $userJobID;
52 * Metadata keyed by field title.
56 protected $metadataByTitle = [];
59 * Get contact type being imported.
63 protected $contactType;
66 * Get contact sub type being imported.
70 protected $contactSubType;
73 * Array of valid relationships for the contact type & subtype.
77 protected $validRelationships = [];
82 * Used for js for quick form.
91 public function getFormName(): string {
92 return $this->formName
;
96 * @param string $formName
98 public function setFormName(string $formName) {
99 $this->formName
= $formName;
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()
113 $this->setValidRelationships($relations);
115 return $this->validRelationships
[$this->getContactType() . '_' . $this->getContactSubType()];
119 * @param array $validRelationships
121 public function setValidRelationships(array $validRelationships) {
122 $this->validRelationships
[$this->getContactType() . '_' . $this->getContactSubType()] = $validRelationships;
126 * Get contact subtype for import.
130 public function getContactSubType(): string {
131 return $this->contactSubType ??
'';
135 * Set contact subtype for import.
137 * @param string $contactSubType
139 public function setContactSubType($contactSubType) {
140 $this->contactSubType
= (string) $contactSubType;
148 protected $mappingID;
153 public function getMetadata(): array {
154 return $this->metadata
;
158 * Setting for metadata.
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.
163 * @param array $metadata
165 * @throws \CiviCRM_API3_Exception
167 public function setMetadata(array $metadata) {
168 $fieldDetails = civicrm_api3('CustomField', 'get', [
169 'return' => ['custom_group_id.title'],
170 'options' => ['limit' => 0],
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'];
179 $this->metadata
= $metadata;
185 public function getMappingID(): int {
186 return $this->mappingID
;
190 * @param int $mappingID
192 public function setMappingID(int $mappingID) {
193 $this->mappingID
= $mappingID;
197 * Get the contact type for the import.
201 public function getContactType(): string {
202 return $this->contactType
;
206 * @param string $contactType
208 public function setContactType(string $contactType) {
209 $this->contactType
= $contactType;
213 * Set the contact type according to the constant.
215 * @param int $contactTypeKey
217 public function setContactTypeByConstant($contactTypeKey) {
219 CRM_Import_Parser
::CONTACT_INDIVIDUAL
=> 'Individual',
220 CRM_Import_Parser
::CONTACT_HOUSEHOLD
=> 'Household',
221 CRM_Import_Parser
::CONTACT_ORGANIZATION
=> 'Organization',
223 $this->contactType
= $constantTypeMap[$contactTypeKey];
227 * Get Mapping Fields.
231 * @throws \CiviCRM_API3_Exception
233 public function getMappingFields(): array {
234 if (empty($this->mappingFields
) && !empty($this->getMappingID())) {
235 $this->loadSavedMapping();
237 return $this->mappingFields
;
241 * Set mapping fields.
243 * We do a little cleanup here too.
245 * We ensure that column numbers are set and that the fields are ordered by them.
247 * This would mean the fields could be loaded unsorted.
249 * @param array $mappingFields
251 public function setMappingFields(array $mappingFields) {
253 foreach ($mappingFields as &$mappingField) {
254 if (!isset($mappingField['column_number'])) {
255 $mappingField['column_number'] = $i;
257 if ($mappingField['column_number'] > $i) {
258 $i = $mappingField['column_number'];
262 $this->mappingFields
= $this->rekeyBySortedColumnNumbers($mappingFields);
266 * Get the names of the mapped fields.
268 * @throws \CiviCRM_API3_Exception
270 public function getFieldNames() {
271 return CRM_Utils_Array
::collect('name', $this->getMappingFields());
275 * Get the field name for the given column.
277 * @param int $columnNumber
280 * @throws \CiviCRM_API3_Exception
282 public function getFieldName($columnNumber) {
283 return $this->getFieldNames()[$columnNumber];
287 * Get the field name for the given column.
289 * @param int $columnNumber
292 * @throws \CiviCRM_API3_Exception
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'];
300 * Get relationship key only if it is valid.
302 * @param int $columnNumber
304 * @return string|null
306 * @throws \CiviCRM_API3_Exception
308 public function getValidRelationshipKey($columnNumber) {
309 $key = $this->getRelationshipKey($columnNumber);
310 return $this->isValidRelationshipKey($key) ?
$key : NULL;
314 * Get the IM Provider ID.
316 * @param int $columnNumber
320 * @throws \CiviCRM_API3_Exception
322 public function getIMProviderID($columnNumber) {
323 return $this->getMappingFields()[$columnNumber]['im_provider_id'] ??
NULL;
329 * @param int $columnNumber
333 * @throws \CiviCRM_API3_Exception
335 public function getPhoneTypeID($columnNumber) {
336 return $this->getMappingFields()[$columnNumber]['phone_type_id'] ??
NULL;
340 * Get the Website Type
342 * @param int $columnNumber
346 * @throws \CiviCRM_API3_Exception
348 public function getWebsiteTypeID($columnNumber) {
349 return $this->getMappingFields()[$columnNumber]['website_type_id'] ??
NULL;
353 * Get the Location Type
355 * Returning 0 rather than null is historical.
357 * @param int $columnNumber
361 * @throws \CiviCRM_API3_Exception
363 public function getLocationTypeID($columnNumber) {
364 return $this->getMappingFields()[$columnNumber]['location_type_id'] ??
0;
368 * Get the IM or Phone type.
370 * We have a field that would be the 'relevant' type - which could be either.
372 * @param int $columnNumber
376 * @throws \CiviCRM_API3_Exception
378 public function getPhoneOrIMTypeID($columnNumber) {
379 return $this->getIMProviderID($columnNumber) ??
$this->getPhoneTypeID($columnNumber);
383 * Get the location types of the mapped fields.
385 * @throws \CiviCRM_API3_Exception
387 public function getFieldLocationTypes() {
388 return CRM_Utils_Array
::collect('location_type_id', $this->getMappingFields());
392 * Get the phone types of the mapped fields.
394 * @throws \CiviCRM_API3_Exception
396 public function getFieldPhoneTypes() {
397 return CRM_Utils_Array
::collect('phone_type_id', $this->getMappingFields());
401 * Get the names of the im_provider fields.
403 * @throws \CiviCRM_API3_Exception
405 public function getFieldIMProviderTypes() {
406 return CRM_Utils_Array
::collect('im_provider_id', $this->getMappingFields());
410 * Get the names of the website fields.
412 * @throws \CiviCRM_API3_Exception
414 public function getFieldWebsiteTypes() {
415 return CRM_Utils_Array
::collect('im_provider_id', $this->getMappingFields());
419 * Get an instance of the importer object.
421 * @return CRM_Contact_Import_Parser_Contact
423 * @throws \CiviCRM_API3_Exception
425 public function getImporterObject() {
426 $importer = new CRM_Contact_Import_Parser_Contact($this->getFieldNames());
427 $importer->setUserJobID($this->getUserJobID());
433 * Load the mapping from the datbase into the format that would be received from the UI.
435 * @throws \CiviCRM_API3_Exception
437 protected function loadSavedMapping() {
438 $fields = civicrm_api3('MappingField', 'get', [
439 'mapping_id' => $this->getMappingID(),
440 'options' => ['limit' => 0],
443 foreach ($fields as $index => $field) {
444 if (!$this->isValidField($field['name'])) {
445 // This scenario could occur if the name of a saved mapping field
446 // changed or became unavailable https://lab.civicrm.org/dev/core/-/issues/3511.
447 $skipped[] = $field['name'];
448 $fields[$index]['name'] = $field['name'] = 'do_not_import';
450 $fieldSpec = $this->getFieldMetadata($field['name']);
451 $fields[$index]['label'] = $fieldSpec['title'];
452 if (empty($field['location_type_id']) && !empty($fieldSpec['hasLocationType'])) {
453 $fields[$index]['location_type_id'] = 'Primary';
456 if (!empty($skipped)) {
457 CRM_Core_Session
::setStatus(ts('Invalid saved mappings were skipped') . ':' . implode(', ', $skipped));
459 $this->mappingFields
= $this->rekeyBySortedColumnNumbers($fields);
463 * Get the metadata for the field.
465 * @param string $fieldName
469 protected function getFieldMetadata(string $fieldName): array {
470 return $this->getMetadata()[$fieldName] ?? CRM_Contact_BAO_Contact
::importableFields('All')[$fieldName];
474 * Is the field valid for this import.
476 * If not defined in metadata is is not valid.
478 * @param string $fieldName
482 public function isValidField(string $fieldName): bool {
483 return isset($this->getMetadata()[$fieldName]) ||
isset(CRM_Contact_BAO_Contact
::importableFields('All')[$fieldName]);
487 * Load the mapping from the database into the pre-5.50 format.
489 * This is preserved as a copy the upgrade script can use - since the
490 * upgrade allows the other to be 'fixed'.
492 * @throws \CiviCRM_API3_Exception
494 protected function legacyLoadSavedMapping() {
495 $fields = civicrm_api3('MappingField', 'get', [
496 'mapping_id' => $this->getMappingID(),
497 'options' => ['limit' => 0],
499 foreach ($fields as $index => $field) {
500 // Fix up the fact that for lost reasons we save by label not name.
501 $fields[$index]['label'] = $field['name'];
502 if (empty($field['relationship_type_id'])) {
503 $fields[$index]['name'] = $this->getNameFromLabel($field['name']);
506 // Honour legacy chaos factor.
507 if ($field['name'] === ts('- do not import -')) {
508 // This is why we save names not labels people....
509 $field['name'] = 'do_not_import';
511 $fields[$index]['name'] = strtolower(str_replace(" ", "_", $field['name']));
512 // fix for edge cases, CRM-4954
513 if ($fields[$index]['name'] === 'image_url') {
514 $fields[$index]['name'] = str_replace('url', 'URL', $fields[$index]['name']);
517 $fieldSpec = $this->getMetadata()[$fields[$index]['name']];
518 if (empty($field['location_type_id']) && !empty($fieldSpec['hasLocationType'])) {
519 $fields[$index]['location_type_id'] = 'Primary';
522 $this->mappingFields
= $this->rekeyBySortedColumnNumbers($fields);
526 * Get the titles from metadata.
528 public function getMetadataTitles() {
529 if (empty($this->metadataByTitle
)) {
530 $this->metadataByTitle
= CRM_Utils_Array
::collect('title', $this->getMetadata());
532 return $this->metadataByTitle
;
536 * Rekey the array by the column_number.
538 * @param array $mappingFields
542 protected function rekeyBySortedColumnNumbers(array $mappingFields) {
543 $this->mappingFields
= CRM_Utils_Array
::rekey($mappingFields, 'column_number');
544 ksort($this->mappingFields
);
545 return $this->mappingFields
;
549 * Get the field name from the label.
551 * @param string $label
555 protected function getNameFromLabel($label) {
556 $titleMap = array_flip($this->getMetadataTitles());
557 $label = str_replace(' (match to contact)', '', $label);
558 return $titleMap[$label] ??
'';
562 * Validate the key against the relationships available for the contatct type & subtype.
568 protected function isValidRelationshipKey($key) {
569 return !empty($this->getValidRelationships()[$key]);
573 * Get the defaults for the column from the saved mapping.
578 * @throws \CiviCRM_API3_Exception
580 public function getSavedQuickformDefaultsForColumn($column) {
583 // $sel1 is either unmapped, a relationship or a target field.
584 if ($this->getFieldName($column) === 'do_not_import') {
585 return $fieldMapping;
588 if ($this->getValidRelationshipKey($column)) {
589 $fieldMapping[] = $this->getValidRelationshipKey($column);
593 $fieldMapping[] = $this->getFieldName($column);
596 if ($this->getWebsiteTypeID($column)) {
597 $fieldMapping[] = $this->getWebsiteTypeID($column);
599 elseif ($this->getLocationTypeID($column)) {
600 $fieldMapping[] = $this->getLocationTypeID($column);
604 if ($this->getPhoneOrIMTypeID($column)) {
605 $fieldMapping[] = $this->getPhoneOrIMTypeID($column);
607 return $fieldMapping;
611 * This exists for use in the FiveFifty Upgrade
613 * @throws \API_Exception|\CiviCRM_API3_Exception
615 public static function convertSavedFields(): void
{
616 $mappings = Mapping
::get(FALSE)
617 ->setSelect(['id', 'contact_type'])
618 ->addWhere('mapping_type_id:name', '=', 'Import Contact')
621 foreach ($mappings as $mapping) {
622 $processor = new CRM_Import_ImportProcessor();
623 $processor->setMappingID($mapping['id']);
624 $processor->setMetadata(CRM_Contact_BAO_Contact
::importableFields('All'));
625 $processor->legacyLoadSavedMapping();;
626 foreach ($processor->getMappingFields() as $field) {
627 // The if is mostly precautionary against running this more than once
628 // - which is common in dev if not live...
629 if ($field['name']) {
630 MappingField
::update(FALSE)
631 ->setValues(['name' => $field['name']])
632 ->addWhere('id', '=', $field['id'])