Skip to content

Commit

Permalink
Merge pull request #17368 from colemanw/api4suffix
Browse files Browse the repository at this point in the history
APIv4 - Improve pseudoconstant suffix support
  • Loading branch information
eileenmcnaughton authored May 21, 2020
2 parents a5949c8 + 3ffbd21 commit 9a71121
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Civi/Api4/Generic/BasicGetAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ protected function formatRawValues(&$records) {
foreach ($records as &$values) {
foreach ($this->entityFields() as $field) {
if (!empty($field['options'])) {
foreach (array_keys(FormattingUtil::$pseudoConstantContexts) as $suffix) {
foreach (FormattingUtil::$pseudoConstantSuffixes as $suffix) {
$pseudofield = $field['name'] . ':' . $suffix;
if (!isset($values[$pseudofield]) && isset($values[$field['name']]) && $this->_isFieldSelected($pseudofield)) {
$values[$pseudofield] = $values[$field['name']];
Expand Down
25 changes: 16 additions & 9 deletions Civi/Api4/Utils/FormattingUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class FormattingUtil {
'label' => 'get',
];

public static $pseudoConstantSuffixes = ['name', 'abbr', 'label', 'color', 'description', 'icon'];

/**
* Massage values into the format the BAO expects for a write operation
*
Expand All @@ -46,7 +48,7 @@ public static function formatWriteParams(&$params, $fields) {
if ($value === 'null') {
$value = 'Null';
}
self::formatInputValue($value, $name, $field);
self::formatInputValue($value, $name, $field, 'create');
// Ensure we have an array for serialized fields
if (!empty($field['serialize'] && !is_array($value))) {
$value = (array) $value;
Expand Down Expand Up @@ -80,19 +82,21 @@ public static function formatWriteParams(&$params, $fields) {
* @param $value
* @param string $fieldName
* @param array $fieldSpec
* @param string $action
* @throws \API_Exception
* @throws \CRM_Core_Exception
*/
public static function formatInputValue(&$value, $fieldName, $fieldSpec) {
public static function formatInputValue(&$value, $fieldName, $fieldSpec, $action = 'get') {
// Evaluate pseudoconstant suffix
$suffix = strpos($fieldName, ':');
if ($suffix) {
$options = self::getPseudoconstantList($fieldSpec['entity'], $fieldSpec['name'], substr($fieldName, $suffix + 1));
$options = self::getPseudoconstantList($fieldSpec['entity'], $fieldSpec['name'], substr($fieldName, $suffix + 1), $action);
$value = self::replacePseudoconstant($options, $value, TRUE);
return;
}
elseif (is_array($value)) {
foreach ($value as &$val) {
self::formatInputValue($val, $fieldName, $fieldSpec);
self::formatInputValue($val, $fieldName, $fieldSpec, $action);
}
return;
}
Expand Down Expand Up @@ -189,17 +193,20 @@ public static function formatOutputValues(&$results, $fields, $entity, $action =
*/
public static function getPseudoconstantList($entity, $fieldName, $valueType, $params = [], $action = 'get') {
$context = self::$pseudoConstantContexts[$valueType] ?? NULL;
if (!$context) {
// For create actions, only unique identifiers can be used.
// For get actions any valid suffix is ok.
if (($action === 'create' && !$context) || !in_array($valueType, self::$pseudoConstantSuffixes, TRUE)) {
throw new \API_Exception('Illegal expression');
}
$baoName = CoreUtil::getBAOFromApiName($entity);
$baoName = $context ? CoreUtil::getBAOFromApiName($entity) : NULL;
// Use BAO::buildOptions if possible
if ($baoName) {
$options = $baoName::buildOptions($fieldName, $context, $params);
}
// Fallback for option lists that exist in the api but not the BAO - note: $valueType gets ignored here
// Fallback for option lists that exist in the api but not the BAO
if (!isset($options) || $options === FALSE) {
$options = civicrm_api4($entity, 'getFields', ['action' => $action, 'loadOptions' => TRUE, 'where' => [['name', '=', $fieldName]]])[0]['options'] ?? NULL;
$options = civicrm_api4($entity, 'getFields', ['action' => $action, 'loadOptions' => ['id', $valueType], 'where' => [['name', '=', $fieldName]]])[0]['options'] ?? NULL;
$options = $options ? array_column($options, $valueType, 'id') : $options;
}
if (is_array($options)) {
return $options;
Expand Down Expand Up @@ -278,7 +285,7 @@ public static function contactFieldsToRemove($contactType, $prefix) {
\Civi::$statics[__CLASS__][__FUNCTION__][$contactType][] = $field['name'];
// Include suffixed variants like prefix_id:label
if (!empty($field['pseudoconstant'])) {
foreach (array_keys(self::$pseudoConstantContexts) as $suffix) {
foreach (self::$pseudoConstantSuffixes as $suffix) {
\Civi::$statics[__CLASS__][__FUNCTION__][$contactType][] = $field['name'] . ':' . $suffix;
}
}
Expand Down
59 changes: 55 additions & 4 deletions tests/phpunit/api/v4/Action/BasicActionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,23 +145,43 @@ public function testBatchFrobnicate() {
public function testGetFields() {
$getFields = MockBasicEntity::getFields()->execute()->indexBy('name');

$this->assertCount(6, $getFields);
$this->assertCount(7, $getFields);
$this->assertEquals('Id', $getFields['id']['title']);
// Ensure default data type is "String" when not specified
$this->assertEquals('String', $getFields['color']['data_type']);

// Getfields should default to loadOptions = false and reduce them to bool
$this->assertTrue($getFields['group']['options']);
$this->assertTrue($getFields['fruit']['options']);
$this->assertFalse($getFields['id']['options']);

// Now load options
// Load simple options
$getFields = MockBasicEntity::getFields()
->addWhere('name', '=', 'group')
->addWhere('name', 'IN', ['group', 'fruit'])
->setLoadOptions(TRUE)
->execute()->indexBy('name');

$this->assertCount(1, $getFields);
$this->assertCount(2, $getFields);
$this->assertArrayHasKey('one', $getFields['group']['options']);
// Complex options should be reduced to simple array
$this->assertArrayHasKey(1, $getFields['fruit']['options']);
$this->assertEquals('Banana', $getFields['fruit']['options'][3]);

// Load complex options
$getFields = MockBasicEntity::getFields()
->addWhere('name', 'IN', ['group', 'fruit'])
->setLoadOptions(['id', 'name', 'label', 'color'])
->execute()->indexBy('name');

// Simple options should be expanded to non-assoc array
$this->assertCount(2, $getFields);
$this->assertEquals('one', $getFields['group']['options'][0]['id']);
$this->assertEquals('First', $getFields['group']['options'][0]['name']);
$this->assertEquals('First', $getFields['group']['options'][0]['label']);
$this->assertFalse(isset($getFields['group']['options'][0]['color']));
// Complex options should give all requested properties
$this->assertEquals('Banana', $getFields['fruit']['options'][2]['label']);
$this->assertEquals('yellow', $getFields['fruit']['options'][2]['color']);
}

public function testItemsToGet() {
Expand Down Expand Up @@ -235,4 +255,35 @@ public function testWildcardSelect() {
$this->assertEquals(['shape', 'size', 'weight'], array_keys($result));
}

public function testPseudoconstantMatch() {
MockBasicEntity::delete()->addWhere('id', '>', 0)->execute();

$records = [
['group:label' => 'First', 'shape' => 'round', 'fruit:name' => 'banana'],
['group:name' => 'Second', 'shape' => 'square', 'fruit:label' => 'Pear'],
];
MockBasicEntity::save()->setRecords($records)->execute();

$results = MockBasicEntity::get()
->addSelect('*', 'group:label', 'group:name', 'fruit:name', 'fruit:color', 'fruit:label')
->execute();

$this->assertEquals('round', $results[0]['shape']);
$this->assertEquals('one', $results[0]['group']);
$this->assertEquals('First', $results[0]['group:label']);
$this->assertEquals('First', $results[0]['group:name']);
$this->assertEquals(3, $results[0]['fruit']);
$this->assertEquals('Banana', $results[0]['fruit:label']);
$this->assertEquals('banana', $results[0]['fruit:name']);
$this->assertEquals('yellow', $results[0]['fruit:color']);

// Cannot match to a non-unique option property like :color on create
try {
MockBasicEntity::create()->addValue('fruit:color', 'yellow')->execute();
}
catch (\API_Exception $createError) {
}
$this->assertContains('Illegal expression', $createError->getMessage());
}

}
23 changes: 23 additions & 0 deletions tests/phpunit/api/v4/Mock/Api4/MockBasicEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ public static function getFields() {
'name' => 'weight',
'data_type' => 'Integer',
],
[
'name' => 'fruit',
'options' => [
[
'id' => 1,
'name' => 'apple',
'label' => 'Apple',
'color' => 'red',
],
[
'id' => 2,
'name' => 'pear',
'label' => 'Pear',
'color' => 'green',
],
[
'id' => 3,
'name' => 'banana',
'label' => 'Banana',
'color' => 'yellow',
],
],
],
];
});
}
Expand Down

0 comments on commit 9a71121

Please sign in to comment.