Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APIv4 Export - Fix logic for exporting pseudoconstant syntax #22201

Merged
merged 1 commit into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 51 additions & 47 deletions Civi/Api4/Generic/ExportAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private function exportRecord(string $entityType, int $entityId, Result $result,
$pseudofields[$field['name'] . '.name'] = $field['name'];
}
// Use pseudoconstant syntax if appropriate
elseif ($this->shouldUsePseudoconstant($field)) {
elseif ($this->shouldUsePseudoconstant($entityType, $field)) {
$select[] = $field['name'] . ':name';
$pseudofields[$field['name'] . ':name'] = $field['name'];
}
Expand Down Expand Up @@ -141,68 +141,72 @@ private function exportRecord(string $entityType, int $entityId, Result $result,
];
// Export entities that reference this one
$daoName = CoreUtil::getInfoItem($entityType, 'dao');
/** @var \CRM_Core_DAO $dao */
$dao = new $daoName();
$dao->id = $entityId;
// Collect references into arrays keyed by entity type
$references = [];
foreach ($dao->findReferences() as $reference) {
$refEntity = \CRM_Utils_Array::first($reference::fields())['entity'] ?? '';
// Limit references by domain
if (property_exists($reference, 'domain_id')) {
if (!isset($reference->domain_id)) {
$reference->find(TRUE);
}
if (isset($reference->domain_id) && $reference->domain_id != $limitRefsByDomain) {
continue;
}
}
$references[$refEntity][] = $reference;
}
foreach ($references as $refEntity => $records) {
$refApiType = CoreUtil::getInfoItem($refEntity, 'type') ?? [];
// Reference must be a ManagedEntity
if (!in_array('ManagedEntity', $refApiType, TRUE)) {
continue;
}
$exclude = [];
// For sortable entities, order by weight and exclude weight from the export (it will be auto-managed)
if (in_array('SortableEntity', $refApiType, TRUE)) {
$exclude[] = $weightCol = CoreUtil::getInfoItem($refEntity, 'order_by');
usort($records, function($a, $b) use ($weightCol) {
if (!isset($a->$weightCol)) {
$a->find(TRUE);
if ($daoName) {
/** @var \CRM_Core_DAO $dao */
$dao = new $daoName();
$dao->id = $entityId;
// Collect references into arrays keyed by entity type
$references = [];
foreach ($dao->findReferences() as $reference) {
$refEntity = \CRM_Utils_Array::first($reference::fields())['entity'] ?? '';
// Limit references by domain
if (property_exists($reference, 'domain_id')) {
if (!isset($reference->domain_id)) {
$reference->find(TRUE);
}
if (!isset($b->$weightCol)) {
$b->find(TRUE);
if (isset($reference->domain_id) && $reference->domain_id != $limitRefsByDomain) {
continue;
}
return $a->$weightCol < $b->$weightCol ? -1 : 1;
});
}
$references[$refEntity][] = $reference;
}
foreach ($records as $record) {
$this->exportRecord($refEntity, $record->id, $result, $name . '_', $exclude);
foreach ($references as $refEntity => $records) {
$refApiType = CoreUtil::getInfoItem($refEntity, 'type') ?? [];
// Reference must be a ManagedEntity
if (!in_array('ManagedEntity', $refApiType, TRUE)) {
continue;
}
$exclude = [];
// For sortable entities, order by weight and exclude weight from the export (it will be auto-managed)
if (in_array('SortableEntity', $refApiType, TRUE)) {
$exclude[] = $weightCol = CoreUtil::getInfoItem($refEntity, 'order_by');
usort($records, function ($a, $b) use ($weightCol) {
if (!isset($a->$weightCol)) {
$a->find(TRUE);
}
if (!isset($b->$weightCol)) {
$b->find(TRUE);
}
return $a->$weightCol < $b->$weightCol ? -1 : 1;
});
}
foreach ($records as $record) {
$this->exportRecord($refEntity, $record->id, $result, $name . '_', $exclude);
}
}
}
}

/**
* 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.
* to use pseudoconstant (field:name) syntax vs plain value.
*
* @param string $entityType
* @param array $field
* @return bool
*/
private function shouldUsePseudoconstant(array $field) {
private function shouldUsePseudoconstant(string $entityType, array $field) {
if (empty($field['options'])) {
return FALSE;
}
$daoName = CoreUtil::getInfoItem($entityType, 'dao');
// Options generated by a callback function tend to be stable,
// and the :name property may not be reliable. Use plain value.
if ($daoName && !empty($daoName::getSupportedFields()[$field['name']]['pseudoconstant']['callback'])) {
return FALSE;
}
// Options with numeric keys probably refer to auto-increment keys
// which vary across different databases. Use :name syntax.
$numericKeys = array_filter(array_keys($field['options']), 'is_numeric');
return count($numericKeys) === count($field['options']);
}
Expand Down
8 changes: 6 additions & 2 deletions tests/phpunit/api/v4/Entity/ManagedEntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ public function testManagedNavigationWeights() {
'permission_operator' => '',
'parent_id.name' => 'Test_Parent',
'is_active' => TRUE,
'has_separator' => NULL,
'has_separator' => 1,
'domain_id' => 'current_domain',
],
],
Expand All @@ -432,7 +432,7 @@ public function testManagedNavigationWeights() {
'permission_operator' => '',
'parent_id.name' => 'Test_Parent',
'is_active' => TRUE,
'has_separator' => NULL,
'has_separator' => 2,
'domain_id' => 'current_domain',
],
],
Expand Down Expand Up @@ -491,6 +491,10 @@ public function testManagedNavigationWeights() {
$this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_1', $nav['export'][1]['name']);
$this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_2', $nav['export'][2]['name']);
$this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_3', $nav['export'][3]['name']);
// The has_separator should be using numeric key not pseudoconstant
$this->assertNull($nav['export'][0]['params']['values']['has_separator']);
$this->assertEquals(1, $nav['export'][1]['params']['values']['has_separator']);
$this->assertEquals(2, $nav['export'][2]['params']['values']['has_separator']);
// Weight should not be included in export of children, leaving it to be auto-managed
$this->assertArrayNotHasKey('weight', $nav['export'][1]['params']['values']);

Expand Down