APIv4 - Add export action to managed entities
[civicrm-core.git] / Civi / Api4 / Generic / ExportAction.php
1 <?php
2
3 /*
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
6 | |
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 +--------------------------------------------------------------------+
11 */
12
13 namespace Civi\Api4\Generic;
14
15 use Civi\API\Exception\NotImplementedException;
16 use Civi\Api4\Utils\CoreUtil;
17
18 /**
19 * Export $ENTITY to civicrm_managed format.
20 *
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.
23 *
24 * @method $this setId(int $id)
25 * @method int getId()
26 * @method $this setCleanup(string $cleanup)
27 * @method string getCleanup()
28 * @method $this setUpdate(string $update)
29 * @method string getUpdate()
30 */
31 class ExportAction extends AbstractAction {
32
33 /**
34 * Id of $ENTITY to export
35 * @var int
36 * @required
37 */
38 protected $id;
39
40 /**
41 * Specify rule for auto-updating managed entity
42 * @var string
43 * @options never,always,unmodified
44 */
45 protected $update = 'unmodified';
46
47 /**
48 * Specify rule for auto-deleting managed entity
49 * @var string
50 * @options never,always,unused
51 */
52 protected $cleanup = 'unused';
53
54 /**
55 * Used to prevent circular references
56 * @var array
57 */
58 private $exportedEntities = [];
59
60 /**
61 * @param \Civi\Api4\Generic\Result $result
62 */
63 public function _run(Result $result) {
64 $this->exportRecord($this->getEntityName(), $this->id, $result);
65 }
66
67 /**
68 * @param string $entityType
69 * @param int $entityId
70 * @param \Civi\Api4\Generic\Result $result
71 * @param string $parentName
72 */
73 private function exportRecord(string $entityType, int $entityId, Result $result, $parentName = NULL) {
74 if (isset($this->exportedEntities[$entityType][$entityId])) {
75 throw new \API_Exception("Circular reference detected: attempted to export $entityType id $entityId multiple times.");
76 }
77 $this->exportedEntities[$entityType][$entityId] = TRUE;
78 $select = $pseudofields = [];
79 $allFields = $this->getFieldsForExport($entityType, TRUE);
80 foreach ($allFields as $field) {
81 // Use implicit join syntax but only if the fk entity has a `name` field
82 if (!empty($field['fk_entity']) && array_key_exists('name', $this->getFieldsForExport($field['fk_entity']))) {
83 $select[] = $field['name'] . '.name';
84 $pseudofields[$field['name'] . '.name'] = $field['name'];
85 }
86 // Use pseudoconstant syntax if appropriate
87 elseif ($this->shouldUsePseudoconstant($field)) {
88 $select[] = $field['name'] . ':name';
89 $pseudofields[$field['name'] . ':name'] = $field['name'];
90 }
91 elseif (empty($field['fk_entity'])) {
92 $select[] = $field['name'];
93 }
94 }
95 $record = civicrm_api4($entityType, 'get', [
96 'checkPermissions' => $this->checkPermissions,
97 'select' => $select,
98 'where' => [['id', '=', $entityId]],
99 ])->first();
100 if (!$record) {
101 return;
102 }
103 // The get api always returns ID, but it should not be included in an export
104 unset($record['id']);
105 // Null fields should not use joins/pseudoconstants
106 foreach ($pseudofields as $alias => $fieldName) {
107 if (is_null($record[$alias])) {
108 unset($record[$alias]);
109 $record[$fieldName] = NULL;
110 }
111 }
112 // Special handing of current_domain
113 foreach ($allFields as $fieldName => $field) {
114 if (($field['fk_entity'] ?? NULL) === 'Domain') {
115 $alias = $fieldName . '.name';
116 if (isset($record[$alias]) && $record[$alias] === \CRM_Core_BAO_Domain::getDomain()->name) {
117 unset($record[$alias]);
118 $record[$fieldName] = 'current_domain';
119 }
120 }
121 }
122 $name = ($parentName ?? '') . $entityType . '_' . ($record['name'] ?? count($this->exportedEntities[$entityType]));
123 $result[] = [
124 'name' => $name,
125 'entity' => $entityType,
126 'cleanup' => $this->cleanup,
127 'update' => $this->update,
128 'params' => [
129 'version' => 4,
130 'values' => $record,
131 ],
132 ];
133 // Export entities that reference this one
134 $daoName = CoreUtil::getInfoItem($entityType, 'dao');
135 /** @var \CRM_Core_DAO $dao */
136 $dao = new $daoName();
137 $dao->id = $entityId;
138 foreach ($dao->findReferences() as $reference) {
139 $refEntity = $reference::fields()['id']['entity'] ?? '';
140 $refApiType = CoreUtil::getInfoItem($refEntity, 'type') ?? [];
141 // Reference must be a ManagedEntity
142 if (in_array('ManagedEntity', $refApiType, TRUE)) {
143 $this->exportRecord($refEntity, $reference->id, $result, $name . '_');
144 }
145 }
146 }
147
148 /**
149 * If a field has a pseudoconstant list, determine whether it would be better
150 * to use pseudoconstant (field:name) syntax.
151 *
152 * Generally speaking, options with numeric keys are the ones we need to worry about
153 * because auto-increment keys can vary when migrating an entity to a different database.
154 *
155 * But options with string keys tend to be stable,
156 * and it's better not to use the pseudoconstant syntax with these fields because
157 * the option list may not be populated at the time of managed entity reconciliation.
158 *
159 * @param array $field
160 * @return bool
161 */
162 private function shouldUsePseudoconstant(array $field) {
163 if (empty($field['options'])) {
164 return FALSE;
165 }
166 $numericKeys = array_filter(array_keys($field['options']), 'is_numeric');
167 return count($numericKeys) === count($field['options']);
168 }
169
170 /**
171 * @param $entityType
172 * @param bool $loadOptions
173 * @return array
174 */
175 private function getFieldsForExport($entityType, $loadOptions = FALSE): array {
176 try {
177 return (array) civicrm_api4($entityType, 'getFields', [
178 'action' => 'create',
179 'where' => [
180 ['type', 'IN', ['Field', 'Custom']],
181 ['readonly', '!=', TRUE],
182 ],
183 'loadOptions' => $loadOptions,
184 'checkPermissions' => $this->checkPermissions,
185 ])->indexBy('name');
186 }
187 catch (NotImplementedException $e) {
188 return [];
189 }
190 }
191
192 }