Skip to content

Commit

Permalink
APIv4 - Add 'suffixes' to getFields metadata
Browse files Browse the repository at this point in the history
This breaks apart the concept of a field having 'options' vs
a field supporting suffixes like campaign_id:label.

It is now possible for a field to not have options but still support suffixes.

This also makes the available suffixes for each field discoverable,
e.g. fields like state_province_id support an :abbr suffix.
  • Loading branch information
colemanw committed Aug 19, 2021
1 parent f75a023 commit ad8d834
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 3 deletions.
26 changes: 26 additions & 0 deletions Civi/Api4/Generic/BasicGetFieldsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ protected function formatResults(&$values, $isInternal) {
if (array_key_exists('label', $fieldDefaults)) {
$field['label'] = $field['label'] ?? $field['title'] ?? $field['name'];
}
if (!empty($field['options']) && empty($field['suffixes']) && array_key_exists('suffixes', $field)) {
$this->setFieldSuffixes($field);
}
if (isset($defaults['options'])) {
$field['options'] = $this->formatOptionList($field['options']);
}
Expand Down Expand Up @@ -183,6 +186,22 @@ private function formatOptionList($options) {
return $formatted;
}

/**
* Set supported field suffixes based on available option keys
* @param array $field
*/
private function setFieldSuffixes(array &$field) {
// These suffixes are always supported if a field has options
$field['suffixes'] = ['name', 'label'];
$firstOption = reset($field['options']);
// If first option is an array, merge in those keys as available suffixes
if (is_array($firstOption)) {
// Remove 'id' because there is no practical reason to use it as a field suffix
$otherKeys = array_diff(array_keys($firstOption), ['id', 'name', 'label']);
$field['suffixes'] = array_merge($field['suffixes'], $otherKeys);
}
}

/**
* @return string
*/
Expand Down Expand Up @@ -275,6 +294,13 @@ public function fields() {
'data_type' => 'Array',
'default_value' => FALSE,
],
[
'name' => 'suffixes',
'data_type' => 'Array',
'default_value' => NULL,
'options' => ['name', 'label', 'description', 'abbr', 'color', 'icon'],
'description' => 'Available option transformations, e.g. :name, :label',
],
[
'name' => 'operators',
'data_type' => 'Array',
Expand Down
1 change: 1 addition & 0 deletions Civi/Api4/Service/Spec/Provider/ContactGetSpecProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function modifySpec(RequestSpec $spec) {
->setType('Filter')
->setOperators(['IN', 'NOT IN'])
->addSqlFilter([__CLASS__, 'getContactGroupSql'])
->setSuffixes(['id', 'name', 'label'])
->setOptionsCallback([__CLASS__, 'getGroupList']);
$spec->addFieldSpec($field);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public function modifySpec(RequestSpec $spec) {
->setType('Filter')
->setOperators(['IN', 'NOT IN'])
->addSqlFilter([__CLASS__, 'getTagFilterSql'])
->setSuffixes(['id', 'name', 'label', 'description', 'color'])
->setOptionsCallback([__CLASS__, 'getTagList']);
$spec->addFieldSpec($field);
}
Expand Down
19 changes: 18 additions & 1 deletion Civi/Api4/Service/Spec/SpecFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public static function arrayToField(array $data, $entity) {
$field->setHelpPost($data['help_post'] ?? NULL);
if (self::customFieldHasOptions($data)) {
$field->setOptionsCallback([__CLASS__, 'getOptions']);
if (!empty($data['option_group_id'])) {
// Option groups support other stuff like description, icon & color,
// but at time of this writing, custom fields do not.
$field->setSuffixes(['id', 'name', 'label']);
}
}
$field->setReadonly($data['is_view']);
}
Expand All @@ -55,7 +60,19 @@ public static function arrayToField(array $data, $entity) {
$field->setTitle($data['title'] ?? NULL);
$field->setLabel($data['html']['label'] ?? NULL);
if (!empty($data['pseudoconstant'])) {
$field->setOptionsCallback([__CLASS__, 'getOptions']);
// Do not load options if 'prefetch' is explicitly FALSE
if ($data['pseudoconstant']['prefetch'] ?? TRUE) {
$field->setOptionsCallback([__CLASS__, 'getOptions']);
}
// These suffixes are always supported if a field has options
$suffixes = ['name', 'label'];
// Add other columns specified in schema (e.g. 'abbrColumn')
foreach (['description', 'abbr', 'icon', 'color'] as $suffix) {
if (isset($data['pseudoconstant'][$suffix . 'Column'])) {
$suffixes[] = $suffix;
}
}
$field->setSuffixes($suffixes);
}
$field->setReadonly(!empty($data['readonly']));
}
Expand Down
15 changes: 15 additions & 0 deletions Civi/Schema/Traits/OptionsSpecTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ trait OptionsSpecTrait {
*/
public $options;

/**
* @var array|null
*/
public $suffixes;

/**
* @var callable
*/
Expand Down Expand Up @@ -59,6 +64,16 @@ public function setOptions($options) {
return $this;
}

/**
* @param array $suffixes
*
* @return $this
*/
public function setSuffixes(?array $suffixes) {
$this->suffixes = $suffixes;
return $this;
}

/**
* @param callable $callback
*
Expand Down
1 change: 1 addition & 0 deletions ext/afform/core/Civi/Api4/Afform.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public static function getFields($checkPermissions = TRUE) {
[
'name' => 'type',
'options' => $self->pseudoconstantOptions('afform_type'),
'suffixes' => ['id', 'name', 'label', 'icon'],
],
[
'name' => 'requires',
Expand Down
4 changes: 4 additions & 0 deletions tests/phpunit/api/v4/Action/BasicActionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ public function testGetFields() {
$this->assertTrue($getFields['fruit']['options']);
$this->assertFalse($getFields['identifier']['options']);

// Getfields should figure out what suffixes are available based on option keys
$this->assertEquals(['name', 'label'], $getFields['group']['suffixes']);
$this->assertEquals(['name', 'label', 'color'], $getFields['fruit']['suffixes']);

// Load simple options
$getFields = MockBasicEntity::getFields()
->addWhere('name', 'IN', ['group', 'fruit'])
Expand Down
16 changes: 14 additions & 2 deletions tests/phpunit/api/v4/Action/GetFieldsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

use api\v4\UnitTestCase;
use Civi\Api4\Contact;
use Civi\Api4\Contribution;

/**
* @group headless
Expand Down Expand Up @@ -66,8 +67,7 @@ public function testComponentFields() {
public function testInternalPropsAreHidden() {
// Public getFields should not contain @internal props
$fields = Contact::getFields(FALSE)
->execute()
->getArrayCopy();
->execute();
foreach ($fields as $field) {
$this->assertArrayNotHasKey('output_formatters', $field);
}
Expand All @@ -79,4 +79,16 @@ public function testInternalPropsAreHidden() {
}
}

public function testPreloadFalse() {
\CRM_Core_BAO_ConfigSetting::enableComponent('CiviContribute');
\CRM_Core_BAO_ConfigSetting::enableComponent('CiviCampaign');
// The campaign_id field has preload = false in the schema,
// Which means the options will NOT load but suffixes are still available
$fields = Contribution::getFields(FALSE)
->setLoadOptions(['name', 'label'])
->execute()->indexBy('name');
$this->assertFalse($fields['campaign_id']['options']);
$this->assertEquals(['name', 'label'], $fields['campaign_id']['suffixes']);
}

}
40 changes: 40 additions & 0 deletions tests/phpunit/api/v4/Action/PseudoconstantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
namespace api\v4\Action;

use Civi\Api4\Address;
use Civi\Api4\Campaign;
use Civi\Api4\Contact;
use Civi\Api4\Activity;
use Civi\Api4\Contribution;
use Civi\Api4\CustomField;
use Civi\Api4\CustomGroup;
use Civi\Api4\Email;
Expand Down Expand Up @@ -298,4 +300,42 @@ public function testParticipantRole() {
$this->assertArrayNotHasKey($participant['id'], (array) $search2);
}

public function testPreloadFalse() {
\CRM_Core_BAO_ConfigSetting::enableComponent('CiviContribute');
\CRM_Core_BAO_ConfigSetting::enableComponent('CiviCampaign');

$contact = $this->createEntity(['type' => 'Individual']);

$campaignTitle = uniqid('Test ');

$campaignId = Campaign::create(FALSE)
->addValue('title', $campaignTitle)
->addValue('campaign_type_id', 1)
->execute()->first()['id'];

$contributionId = Contribution::create(FALSE)
->addValue('campaign_id', $campaignId)
->addValue('contact_id', $contact['id'])
->addValue('financial_type_id', 1)
->addValue('total_amount', .01)
->execute()->first()['id'];

// Even though the option list of campaigns is not available (prefetch = false)
// We should still be able to get the title of the campaign as :label
$result = Contribution::get(FALSE)
->addWhere('id', '=', $contributionId)
->addSelect('campaign_id:label')
->execute()->single();

$this->assertEquals($campaignTitle, $result['campaign_id:label']);

// Fetching the title via join ought to work too
$result = Contribution::get(FALSE)
->addWhere('id', '=', $contributionId)
->addSelect('campaign_id.title')
->execute()->single();

$this->assertEquals($campaignTitle, $result['campaign_id.title']);
}

}

0 comments on commit ad8d834

Please sign in to comment.