4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
13 namespace Civi\Api4\Generic
;
15 use Civi\API\Exception\NotImplementedException
;
16 use Civi\Api4\Utils\CoreUtil
;
19 * Export $ENTITY to civicrm_managed format.
21 * This action generates an exportable array suitable for use in a .mgd.php file.
22 * The array will include any other entities that reference the $ENTITY.
24 * @method $this setId(int $id)
26 * @method $this setCleanup(string $cleanup)
27 * @method string getCleanup()
28 * @method $this setUpdate(string $update)
29 * @method string getUpdate()
31 class ExportAction
extends AbstractAction
{
34 * Id of $ENTITY to export
41 * Specify rule for auto-updating managed entity
43 * @options never,always,unmodified
45 protected $update = 'unmodified';
48 * Specify rule for auto-deleting managed entity
50 * @options never,always,unused
52 protected $cleanup = 'unused';
55 * Used to prevent circular references
58 private $exportedEntities = [];
61 * @param \Civi\Api4\Generic\Result $result
63 public function _run(Result
$result) {
64 $this->exportRecord($this->getEntityName(), $this->id
, $result);
68 * @param string $entityType
69 * @param int $entityId
70 * @param \Civi\Api4\Generic\Result $result
71 * @param string $parentName
72 * @param array $excludeFields
74 private function exportRecord(string $entityType, int $entityId, Result
$result, $parentName = NULL, $excludeFields = []) {
75 if (isset($this->exportedEntities
[$entityType][$entityId])) {
76 throw new \
API_Exception("Circular reference detected: attempted to export $entityType id $entityId multiple times.");
78 $this->exportedEntities
[$entityType][$entityId] = TRUE;
79 $select = $pseudofields = [];
80 $allFields = $this->getFieldsForExport($entityType, TRUE, $excludeFields);
81 foreach ($allFields as $field) {
82 // Use implicit join syntax but only if the fk entity has a `name` field
83 if (!empty($field['fk_entity']) && array_key_exists('name', $this->getFieldsForExport($field['fk_entity']))) {
84 $select[] = $field['name'];
85 $select[] = $field['name'] . '.name';
86 $pseudofields[$field['name'] . '.name'] = $field['name'];
88 // Use pseudoconstant syntax if appropriate
89 elseif ($this->shouldUsePseudoconstant($entityType, $field)) {
90 $select[] = $field['name'];
91 $select[] = $field['name'] . ':name';
92 $pseudofields[$field['name'] . ':name'] = $field['name'];
94 elseif (empty($field['fk_entity'])) {
95 $select[] = $field['name'];
98 $record = civicrm_api4($entityType, 'get', [
99 'checkPermissions' => $this->checkPermissions
,
101 'where' => [['id', '=', $entityId]],
106 // The get api always returns ID, but it should not be included in an export
107 unset($record['id']);
108 // Should references be limited to the current domain?
109 $limitRefsByDomain = $entityType === 'OptionGroup' && \CRM_Core_OptionGroup
::isDomainOptionGroup($record['name']) ? \CRM_Core_BAO_Domain
::getDomain()->id
: FALSE;
110 foreach ($allFields as $fieldName => $field) {
111 if (($field['fk_entity'] ??
NULL) === 'Domain') {
112 $alias = $fieldName . '.name';
113 if (isset($record[$alias])) {
114 // If this entity is for a specific domain, limit references to that same domain
115 if ($fieldName === 'domain_id') {
116 $limitRefsByDomain = \CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_Domain', $record[$alias], 'id', 'name');
118 // Swap current domain for special API keyword
119 if ($record[$alias] === \CRM_Core_BAO_Domain
::getDomain()->name
) {
120 unset($record[$alias]);
121 $record[$fieldName] = 'current_domain';
126 $name = ($parentName ??
'') . $entityType . '_' . ($record['name'] ??
count($this->exportedEntities
[$entityType]));
127 // Ensure safe characters, max length
128 $name = \CRM_Utils_String
::munge($name, '_', 127);
129 // Include option group with custom field
130 if ($entityType === 'CustomField') {
132 !empty($record['option_group_id.name']) &&
133 // Sometimes fields share an option group; only export it once.
134 empty($this->exportedEntities
['OptionGroup'][$record['option_group_id']])
136 $this->exportRecord('OptionGroup', $record['option_group_id'], $result);
139 // Don't use joins/pseudoconstants if null or if it has the same value as the original
140 foreach ($pseudofields as $alias => $fieldName) {
141 if (!isset($record[$alias]) ||
$record[$alias] == ($record[$fieldName] ??
NULL)) {
142 unset($record[$alias]);
145 unset($record[$fieldName]);
150 'entity' => $entityType,
151 'cleanup' => $this->cleanup
,
152 'update' => $this->update
,
158 // Export entities that reference this one
159 $daoName = CoreUtil
::getInfoItem($entityType, 'dao');
161 /** @var \CRM_Core_DAO $dao */
162 $dao = new $daoName();
163 $dao->id
= $entityId;
164 // Collect references into arrays keyed by entity type
166 foreach ($dao->findReferences() as $reference) {
167 $refEntity = \CRM_Utils_Array
::first($reference::fields())['entity'] ??
'';
168 // Custom fields don't really "belong" to option groups despite the reference
169 if ($refEntity === 'CustomField' && $entityType === 'OptionGroup') {
172 // Limit references by domain
173 if (property_exists($reference, 'domain_id')) {
174 if (!isset($reference->domain_id
)) {
175 $reference->find(TRUE);
177 if (isset($reference->domain_id
) && $reference->domain_id
!= $limitRefsByDomain) {
181 $references[$refEntity][] = $reference;
183 foreach ($references as $refEntity => $records) {
184 $refApiType = CoreUtil
::getInfoItem($refEntity, 'type') ??
[];
185 // Reference must be a ManagedEntity
186 if (!in_array('ManagedEntity', $refApiType, TRUE)) {
190 // For sortable entities, order by weight and exclude weight from the export (it will be auto-managed)
191 if (in_array('SortableEntity', $refApiType, TRUE)) {
192 $exclude[] = $weightCol = CoreUtil
::getInfoItem($refEntity, 'order_by');
193 usort($records, function ($a, $b) use ($weightCol) {
194 if (!isset($a->$weightCol)) {
197 if (!isset($b->$weightCol)) {
200 return $a->$weightCol < $b->$weightCol ?
-1 : 1;
203 foreach ($records as $record) {
204 $this->exportRecord($refEntity, $record->id
, $result, $name . '_', $exclude);
211 * If a field has a pseudoconstant list, determine whether it would be better
212 * to use pseudoconstant (field:name) syntax vs plain value.
214 * @param string $entityType
215 * @param array $field
218 private function shouldUsePseudoconstant(string $entityType, array $field) {
219 if (empty($field['options'])) {
222 $daoName = CoreUtil
::getInfoItem($entityType, 'dao');
223 // Exception for Profile.field_name
224 if ($entityType === 'UFField' && $field['name'] === 'field_name') {
227 // Options generated by a callback function tend to be stable,
228 // and the :name property may not be reliable. Use plain value.
229 if ($daoName && !empty($daoName::getSupportedFields()[$field['name']]['pseudoconstant']['callback'])) {
232 // Options with numeric keys probably refer to auto-increment keys
233 // which vary across different databases. Use :name syntax.
234 $numericKeys = array_filter(array_keys($field['options']), 'is_numeric');
235 return count($numericKeys) === count($field['options']);
240 * @param bool $loadOptions
241 * @param array $excludeFields
244 private function getFieldsForExport($entityType, $loadOptions = FALSE, $excludeFields = []): array {
246 ['type', 'IN', ['Field', 'Custom']],
247 ['readonly', '!=', TRUE],
249 if ($excludeFields) {
250 $conditions[] = ['name', 'NOT IN', $excludeFields];
253 return (array) civicrm_api4($entityType, 'getFields', [
254 'action' => 'create',
255 'where' => $conditions,
256 'loadOptions' => $loadOptions,
257 'checkPermissions' => $this->checkPermissions
,
260 catch (NotImplementedException
$e) {