Skip to content

Commit

Permalink
APIv4 - Add export action to managed entities
Browse files Browse the repository at this point in the history
This action generates an exportable array suitable for use in a .mgd.php file.
  • Loading branch information
colemanw committed Nov 9, 2021
1 parent 2b05996 commit f297cf3
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 2 deletions.
3 changes: 1 addition & 2 deletions CRM/Core/DAO.php
Original file line number Diff line number Diff line change
Expand Up @@ -2523,8 +2523,7 @@ public static function createReferenceColumns($className) {
/**
* Find all records which refer to this entity.
*
* @return array
* Array of objects referencing this
* @return CRM_Core_DAO[]
*/
public function findReferences() {
$links = self::getReferencesToTable(static::getTableName());
Expand Down
135 changes: 135 additions & 0 deletions Civi/Api4/Generic/ExportAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

namespace Civi\Api4\Generic;

use Civi\Api4\Utils\CoreUtil;

/**
* Export $ENTITY to civicrm_managed format.
*
* This action generates an exportable array suitable for use
* in a .mgd.php file.
*
* @method $this setId(int $id)
* @method int getId()
* @method $this setCleanup(string $cleanup)
* @method string getCleanup()
* @method $this setUpdate(string $update)
* @method string getUpdate()
*/
class ExportAction extends AbstractAction {

/**
* Id of $ENTITY to export
* @var int
* @required
*/
protected $id;

/**
* Specify rule for auto-updating managed entity
* @var string
* @options never,always,unmodified
*/
protected $update = 'unmodified';

/**
* Specify rule for auto-deleting managed entity
* @var string
* @options never,always,unused,unmodified
*/
protected $cleanup = 'unmodified';

/**
* Used to prevent circular references
* @var array
*/
private $exportedEntities = [];

/**
* @param \Civi\Api4\Generic\Result $result
*/
public function _run(Result $result) {
$this->exportRecord($this->getEntityName(), $this->id, $result);
}

/**
* @param string $entityType
* @param int $entityId
* @param \Civi\Api4\Generic\Result $result
*/
private function exportRecord(string $entityType, int $entityId, Result $result) {
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 = [];
foreach ($this->getFieldsForExport($entityType) as $field) {
if (!empty($field['fk_entity'])) {
if (array_key_exists('name', $this->getFieldsForExport($field['fk_entity']))) {
$select[] = $field['name'] . '.name';
}
}
else {
$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']);
$name = $record['name'] ?? count($this->exportedEntities[$entityType]);
$result[] = [
'name' => $entityType . '_' . $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, and not the same type as the current entity
if ($refEntity !== $entityType && in_array('ManagedEntity', $refApiType, TRUE)) {
$this->exportRecord($refEntity, $reference->id, $result);
}
}
}

/**
* @param $entityType
* @return array
*/
private function getFieldsForExport($entityType): array {
return (array) civicrm_api4($entityType, 'getFields', [
'action' => 'create',
'where' => [['type', 'IN', ['Field', 'Custom']]],
'checkPermissions' => $this->checkPermissions,
])->indexBy('name');
}

}
10 changes: 10 additions & 0 deletions Civi/Api4/Generic/Traits/ManagedEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Civi\Api4\Generic\Traits;

use Civi\Api4\Generic\BasicBatchAction;
use Civi\Api4\Generic\ExportAction;

/**
* A managed entity includes extra fields and methods to revert from an overridden local to base state.
Expand All @@ -36,4 +37,13 @@ public static function revert($checkPermissions = TRUE) {
}))->setCheckPermissions($checkPermissions);
}

/**
* @param bool $checkPermissions
* @return \Civi\Api4\Generic\ExportAction
*/
public static function export($checkPermissions = TRUE) {
return (new ExportAction(static::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}

}
101 changes: 101 additions & 0 deletions ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchExportTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php
namespace api\v4\SearchDisplay;

use Civi\Api4\SavedSearch;
use Civi\Api4\SearchDisplay;
use Civi\Test\HeadlessInterface;
use Civi\Test\TransactionalInterface;

/**
* @group headless
*/
class SearchExportTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface {

public function setUpHeadless() {
// Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
// See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}

/**
* Test running a searchDisplay within an afform.
*/
public function testExportSearch() {
$search = SavedSearch::create(FALSE)
->setValues([
'name' => 'TestSearchToExport',
'label' => 'TestSearchToExport',
'api_entity' => 'Contact',
'api_params' => [
'version' => 4,
'select' => ['id'],
],
])
->execute()->first();

SearchDisplay::create(FALSE)
->setValues([
'name' => 'TestDisplayToExport',
'label' => 'TestDisplayToExport',
'saved_search_id.name' => 'TestSearchToExport',
'type' => 'table',
'settings' => [
'columns' => [
[
'key' => 'id',
'label' => 'Contact ID',
'dataType' => 'Integer',
'type' => 'field',
],
],
],
'acl_bypass' => FALSE,
])
->execute();

$export = SavedSearch::export(FALSE)
->setId($search['id'])
->execute()
->indexBy('name');

$this->assertCount(2, $export);
// The savedSearch should be first before its reference entities
$this->assertEquals('SavedSearch', $export->first()['entity']);
// Ensure api version is set to 4
$this->assertEquals(4, $export['SavedSearch_TestSearchToExport']['params']['version']);
// Ensure FK is set correctly
$this->assertEquals('TestSearchToExport', $export['SearchDisplay_TestDisplayToExport']['params']['values']['saved_search_id.name']);

// Add a second display
SearchDisplay::create(FALSE)
->setValues([
'name' => 'SecondDisplayToExport',
'label' => 'TestDisplayToExport',
'saved_search_id.name' => 'TestSearchToExport',
'type' => 'table',
'settings' => [
'columns' => [
[
'key' => 'id',
'label' => 'Contact ID',
'dataType' => 'Integer',
'type' => 'field',
],
],
],
'acl_bypass' => FALSE,
])
->execute();

$export = SavedSearch::export(FALSE)
->setId($search['id'])
->execute()
->indexBy('name');

$this->assertCount(3, $export);
$this->assertEquals('TestSearchToExport', $export['SearchDisplay_SecondDisplayToExport']['params']['values']['saved_search_id.name']);
}

}

0 comments on commit f297cf3

Please sign in to comment.