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],
442 foreach ($fields as $index => $field) {
443 $fieldSpec = $this->getMetadata()[$fields[$index]['name']];
444 $fields[$index]['label'] = $fieldSpec['title'];
445 if (empty($field['location_type_id']) && !empty($fieldSpec['hasLocationType'])) {
446 $fields[$index]['location_type_id'] = 'Primary';
449 $this->mappingFields
= $this->rekeyBySortedColumnNumbers($fields);
453 * Load the mapping from the database into the pre-5.50 format.
455 * This is preserved as a copy the upgrade script can use - since the
456 * upgrade allows the other to be 'fixed'.
458 * @throws \CiviCRM_API3_Exception
460 protected function legacyLoadSavedMapping() {
461 $fields = civicrm_api3('MappingField', 'get', [
462 'mapping_id' => $this->getMappingID(),
463 'options' => ['limit' => 0],
465 foreach ($fields as $index => $field) {
466 // Fix up the fact that for lost reasons we save by label not name.
467 $fields[$index]['label'] = $field['name'];
468 if (empty($field['relationship_type_id'])) {
469 $fields[$index]['name'] = $this->getNameFromLabel($field['name']);
472 // Honour legacy chaos factor.
473 if ($field['name'] === ts('- do not import -')) {
474 // This is why we save names not labels people....
475 $field['name'] = 'do_not_import';
477 $fields[$index]['name'] = strtolower(str_replace(" ", "_", $field['name']));
478 // fix for edge cases, CRM-4954
479 if ($fields[$index]['name'] === 'image_url') {
480 $fields[$index]['name'] = str_replace('url', 'URL', $fields[$index]['name']);
483 $fieldSpec = $this->getMetadata()[$fields[$index]['name']];
484 if (empty($field['location_type_id']) && !empty($fieldSpec['hasLocationType'])) {
485 $fields[$index]['location_type_id'] = 'Primary';
488 $this->mappingFields
= $this->rekeyBySortedColumnNumbers($fields);
492 * Get the titles from metadata.
494 public function getMetadataTitles() {
495 if (empty($this->metadataByTitle
)) {
496 $this->metadataByTitle
= CRM_Utils_Array
::collect('title', $this->getMetadata());
498 return $this->metadataByTitle
;
502 * Rekey the array by the column_number.
504 * @param array $mappingFields
508 protected function rekeyBySortedColumnNumbers(array $mappingFields) {
509 $this->mappingFields
= CRM_Utils_Array
::rekey($mappingFields, 'column_number');
510 ksort($this->mappingFields
);
511 return $this->mappingFields
;
515 * Get the field name from the label.
517 * @param string $label
521 protected function getNameFromLabel($label) {
522 $titleMap = array_flip($this->getMetadataTitles());
523 $label = str_replace(' (match to contact)', '', $label);
524 return $titleMap[$label] ??
'';
528 * Validate the key against the relationships available for the contatct type & subtype.
534 protected function isValidRelationshipKey($key) {
535 return !empty($this->getValidRelationships()[$key]);
539 * Get the relevant js for quickform.
544 * @throws \CiviCRM_API3_Exception
546 public function getQuickFormJSForField($column) {
547 $columnNumbersToHide = [];
548 if ($this->getFieldName($column) === 'do_not_import') {
549 $columnNumbersToHide = [1, 2, 3];
551 elseif ($this->getRelationshipKey($column)) {
552 if (!$this->getWebsiteTypeID($column) && !$this->getLocationTypeID($column)) {
553 $columnNumbersToHide[] = 2;
555 if (!$this->getFieldName($column)) {
556 $columnNumbersToHide[] = 1;
558 if (!$this->getPhoneOrIMTypeID($column)) {
559 $columnNumbersToHide[] = 3;
563 if (!$this->getLocationTypeID($column) && !$this->getWebsiteTypeID($column)) {
564 $columnNumbersToHide[] = 1;
566 if (!$this->getPhoneOrIMTypeID($column)) {
567 $columnNumbersToHide[] = 2;
569 $columnNumbersToHide[] = 3;
573 foreach ($columnNumbersToHide as $columnNumber) {
574 $jsClauses[] = $this->getFormName() . "['mapper[$column][" . $columnNumber . "]'].style.display = 'none';";
576 return empty($jsClauses) ?
'' : implode("\n", $jsClauses) . "\n";
580 * Get the defaults for the column from the saved mapping.
585 * @throws \CiviCRM_API3_Exception
587 public function getSavedQuickformDefaultsForColumn($column) {
588 if ($this->getFieldName($column) === 'do_not_import') {
591 if ($this->getValidRelationshipKey($column)) {
592 if ($this->getWebsiteTypeID($column)) {
593 return [$this->getValidRelationshipKey($column), $this->getFieldName($column), $this->getWebsiteTypeID($column)];
595 return [$this->getValidRelationshipKey($column), $this->getFieldName($column), $this->getLocationTypeID($column), $this->getPhoneOrIMTypeID($column)];
597 if ($this->getWebsiteTypeID($column)) {
598 return [$this->getFieldName($column), $this->getWebsiteTypeID($column)];
600 return [(string) $this->getFieldName($column), $this->getLocationTypeID($column), $this->getPhoneOrIMTypeID($column)];
604 * This exists for use in the FiveFifty Upgrade
606 * @throws \API_Exception|\CiviCRM_API3_Exception
608 public static function convertSavedFields(): void
{
609 $mappings = Mapping
::get(FALSE)
610 ->setSelect(['id', 'contact_type'])
611 ->addWhere('mapping_type_id:name', '=', 'Import Contact')
614 foreach ($mappings as $mapping) {
615 $processor = new CRM_Import_ImportProcessor();
616 $processor->setMappingID($mapping['id']);
617 $processor->setMetadata(CRM_Contact_BAO_Contact
::importableFields('All'));
618 $processor->legacyLoadSavedMapping();;
619 foreach ($processor->getMappingFields() as $field) {
620 // The if is mostly precautionary against running this more than once
621 // - which is common in dev if not live...
622 if ($field['name']) {
623 MappingField
::update(FALSE)
624 ->setValues(['name' => $field['name']])
625 ->addWhere('id', '=', $field['id'])