exportRecord($this->getEntityName(), $this->id, $result); } /** * @param string $entityType * @param int $entityId * @param \Civi\Api4\Generic\Result $result * @param string $parentName */ private function exportRecord(string $entityType, int $entityId, Result $result, $parentName = NULL) { if (isset($this->exportedEntities[$entityType][$entityId])) { throw new \API_Exception("Circular reference detected: attempted to export $entityType id $entityId multiple times."); } $this->exportedEntities[$entityType][$entityId] = TRUE; $select = $pseudofields = []; $allFields = $this->getFieldsForExport($entityType, TRUE); foreach ($allFields as $field) { // Use implicit join syntax but only if the fk entity has a `name` field if (!empty($field['fk_entity']) && array_key_exists('name', $this->getFieldsForExport($field['fk_entity']))) { $select[] = $field['name'] . '.name'; $pseudofields[$field['name'] . '.name'] = $field['name']; } // Use pseudoconstant syntax if appropriate elseif ($this->shouldUsePseudoconstant($field)) { $select[] = $field['name'] . ':name'; $pseudofields[$field['name'] . ':name'] = $field['name']; } elseif (empty($field['fk_entity'])) { $select[] = $field['name']; } } $record = civicrm_api4($entityType, 'get', [ 'checkPermissions' => $this->checkPermissions, 'select' => $select, 'where' => [['id', '=', $entityId]], ])->first(); if (!$record) { return; } // The get api always returns ID, but it should not be included in an export unset($record['id']); // Null fields should not use joins/pseudoconstants foreach ($pseudofields as $alias => $fieldName) { if (is_null($record[$alias])) { unset($record[$alias]); $record[$fieldName] = NULL; } } // Special handing of current_domain foreach ($allFields as $fieldName => $field) { if (($field['fk_entity'] ?? NULL) === 'Domain') { $alias = $fieldName . '.name'; if (isset($record[$alias]) && $record[$alias] === \CRM_Core_BAO_Domain::getDomain()->name) { unset($record[$alias]); $record[$fieldName] = 'current_domain'; } } } $name = ($parentName ?? '') . $entityType . '_' . ($record['name'] ?? count($this->exportedEntities[$entityType])); $result[] = [ 'name' => $name, 'entity' => $entityType, 'cleanup' => $this->cleanup, 'update' => $this->update, 'params' => [ 'version' => 4, 'values' => $record, ], ]; // Export entities that reference this one $daoName = CoreUtil::getInfoItem($entityType, 'dao'); /** @var \CRM_Core_DAO $dao */ $dao = new $daoName(); $dao->id = $entityId; foreach ($dao->findReferences() as $reference) { $refEntity = $reference::fields()['id']['entity'] ?? ''; $refApiType = CoreUtil::getInfoItem($refEntity, 'type') ?? []; // Reference must be a ManagedEntity if (in_array('ManagedEntity', $refApiType, TRUE)) { $this->exportRecord($refEntity, $reference->id, $result, $name . '_'); } } } /** * If a field has a pseudoconstant list, determine whether it would be better * to use pseudoconstant (field:name) syntax. * * Generally speaking, options with numeric keys are the ones we need to worry about * because auto-increment keys can vary when migrating an entity to a different database. * * But options with string keys tend to be stable, * and it's better not to use the pseudoconstant syntax with these fields because * the option list may not be populated at the time of managed entity reconciliation. * * @param array $field * @return bool */ private function shouldUsePseudoconstant(array $field) { if (empty($field['options'])) { return FALSE; } $numericKeys = array_filter(array_keys($field['options']), 'is_numeric'); return count($numericKeys) === count($field['options']); } /** * @param $entityType * @param bool $loadOptions * @return array */ private function getFieldsForExport($entityType, $loadOptions = FALSE): array { try { return (array) civicrm_api4($entityType, 'getFields', [ 'action' => 'create', 'where' => [ ['type', 'IN', ['Field', 'Custom']], ['readonly', '!=', TRUE], ], 'loadOptions' => $loadOptions, 'checkPermissions' => $this->checkPermissions, ])->indexBy('name'); } catch (NotImplementedException $e) { return []; } } }