From 8031eb8ed84efa17ba80811d3cc236d8c4eb43d4 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sat, 30 Jul 2022 21:41:17 -0400 Subject: [PATCH] APIv4 - Improve pseudoconstant support in getFields This allows APIv4 getFields arrays to specify a pseudoconstant key which is automatically transformed into an array of options in the right format. --- Civi/Api4/Entity.php | 222 ++++++++---------- Civi/Api4/Generic/BasicGetFieldsAction.php | 72 ++++-- Civi/Api4/Service/Spec/SpecFormatter.php | 24 +- Civi/Api4/Utils/CoreUtil.php | 48 ++++ ext/afform/core/Civi/Api4/Afform.php | 5 +- .../Civi/Afform/AfformGetFieldsTest.php | 27 +++ 6 files changed, 231 insertions(+), 167 deletions(-) create mode 100644 ext/afform/core/tests/phpunit/Civi/Afform/AfformGetFieldsTest.php diff --git a/Civi/Api4/Entity.php b/Civi/Api4/Entity.php index 3d362edfe9..a53248cf3a 100644 --- a/Civi/Api4/Entity.php +++ b/Civi/Api4/Entity.php @@ -21,6 +21,108 @@ namespace Civi\Api4; */ class Entity extends Generic\AbstractEntity { + /** + * @var array[] + */ + public static $entityFields = [ + [ + 'name' => 'name', + 'description' => 'Entity name', + ], + [ + 'name' => 'title', + 'description' => 'Localized title (singular)', + ], + [ + 'name' => 'title_plural', + 'description' => 'Localized title (plural)', + ], + [ + 'name' => 'type', + 'data_type' => 'Array', + 'description' => 'Base class for this entity', + 'pseudoconstant' => ['callback' => ['Civi\Api4\Utils\CoreUtil', 'getEntityTypes']], + ], + [ + 'name' => 'description', + 'description' => 'Description from docblock', + ], + [ + 'name' => 'comment', + 'description' => 'Comments from docblock', + ], + [ + 'name' => 'icon', + 'description' => 'crm-i icon class associated with this entity', + ], + [ + 'name' => 'dao', + 'description' => 'Class name for dao-based entities', + ], + [ + 'name' => 'table_name', + 'description' => 'Name of sql table, if applicable', + ], + [ + 'name' => 'primary_key', + 'data_type' => 'Array', + 'description' => 'Name of unique identifier field(s) (e.g. [id])', + ], + [ + 'name' => 'label_field', + 'description' => 'Field to show when displaying a record', + ], + [ + 'name' => 'order_by', + 'description' => 'Default column to sort results', + ], + [ + 'name' => 'searchable', + 'description' => 'How should this entity be presented in search UIs', + 'pseudoconstant' => ['callback' => ['Civi\Api4\Utils\CoreUtil', 'getSearchableOptions']], + ], + [ + 'name' => 'paths', + 'data_type' => 'Array', + 'description' => 'System paths for accessing this entity', + ], + [ + 'name' => 'see', + 'data_type' => 'Array', + 'description' => 'Any @see annotations from docblock', + ], + [ + 'name' => 'since', + 'data_type' => 'String', + 'description' => 'Version this API entity was added', + ], + [ + 'name' => 'class', + 'data_type' => 'String', + 'description' => 'PHP class name', + ], + [ + 'name' => 'class_args', + 'data_type' => 'Array', + 'description' => 'Arguments needed by php action factory functions (used when multiple entities share a class, e.g. CustomValue).', + ], + [ + 'name' => 'bridge', + 'data_type' => 'Array', + 'description' => 'Connecting fields for EntityBridge types', + ], + [ + 'name' => 'ui_join_filters', + 'data_type' => 'Array', + 'description' => 'When joining entities in the UI, which fields should be presented by default in the ON clause', + ], + [ + 'name' => 'group_weights_by', + 'data_type' => 'Array', + 'description' => 'For sortable entities, what field groupings are used to order by weight', + ], + ]; + /** * @param bool $checkPermissions * @return Action\Entity\Get @@ -36,109 +138,7 @@ class Entity extends Generic\AbstractEntity { */ public static function getFields($checkPermissions = TRUE) { return (new Generic\BasicGetFieldsAction('Entity', __FUNCTION__, function(Generic\BasicGetFieldsAction $getFields) { - return [ - [ - 'name' => 'name', - 'description' => 'Entity name', - ], - [ - 'name' => 'title', - 'description' => 'Localized title (singular)', - ], - [ - 'name' => 'title_plural', - 'description' => 'Localized title (plural)', - ], - [ - 'name' => 'type', - 'data_type' => 'Array', - 'description' => 'Base class for this entity', - 'options' => $getFields->getLoadOptions() ? self::getEntityTypes() : TRUE, - ], - [ - 'name' => 'description', - 'description' => 'Description from docblock', - ], - [ - 'name' => 'comment', - 'description' => 'Comments from docblock', - ], - [ - 'name' => 'icon', - 'description' => 'crm-i icon class associated with this entity', - ], - [ - 'name' => 'dao', - 'description' => 'Class name for dao-based entities', - ], - [ - 'name' => 'table_name', - 'description' => 'Name of sql table, if applicable', - ], - [ - 'name' => 'primary_key', - 'data_type' => 'Array', - 'description' => 'Name of unique identifier field(s) (e.g. [id])', - ], - [ - 'name' => 'label_field', - 'description' => 'Field to show when displaying a record', - ], - [ - 'name' => 'order_by', - 'description' => 'Default column to sort results', - ], - [ - 'name' => 'searchable', - 'description' => 'How should this entity be presented in search UIs', - 'options' => [ - 'primary' => ts('Primary'), - 'secondary' => ts('Secondary'), - 'bridge' => ts('Bridge'), - 'none' => ts('None'), - ], - ], - [ - 'name' => 'paths', - 'data_type' => 'Array', - 'description' => 'System paths for accessing this entity', - ], - [ - 'name' => 'see', - 'data_type' => 'Array', - 'description' => 'Any @see annotations from docblock', - ], - [ - 'name' => 'since', - 'data_type' => 'String', - 'description' => 'Version this API entity was added', - ], - [ - 'name' => 'class', - 'data_type' => 'String', - 'description' => 'PHP class name', - ], - [ - 'name' => 'class_args', - 'data_type' => 'Array', - 'description' => 'Arguments needed by php action factory functions (used when multiple entities share a class, e.g. CustomValue).', - ], - [ - 'name' => 'bridge', - 'data_type' => 'Array', - 'description' => 'Connecting fields for EntityBridge types', - ], - [ - 'name' => 'ui_join_filters', - 'data_type' => 'Array', - 'description' => 'When joining entities in the UI, which fields should be presented by default in the ON clause', - ], - [ - 'name' => 'group_weights_by', - 'data_type' => 'Array', - 'description' => 'For sortable entities, what field groupings are used to order by weight', - ], - ]; + return Entity::$entityFields; }))->setCheckPermissions($checkPermissions); } @@ -161,20 +161,4 @@ class Entity extends Generic\AbstractEntity { ]; } - /** - * Collect the 'type' values from every entity. - * - * @return array - */ - private static function getEntityTypes() { - $provider = \Civi::service('action_object_provider'); - $entityTypes = []; - foreach ($provider->getEntities() as $entity) { - foreach ($entity['type'] ?? [] as $type) { - $entityTypes[$type] = $type; - } - } - return $entityTypes; - } - } diff --git a/Civi/Api4/Generic/BasicGetFieldsAction.php b/Civi/Api4/Generic/BasicGetFieldsAction.php index 494af4829c..3645282231 100644 --- a/Civi/Api4/Generic/BasicGetFieldsAction.php +++ b/Civi/Api4/Generic/BasicGetFieldsAction.php @@ -13,6 +13,7 @@ namespace Civi\Api4\Generic; use Civi\API\Exception\NotImplementedException; +use Civi\Api4\Utils\CoreUtil; /** * Lists information about fields for the $ENTITY entity. @@ -139,51 +140,72 @@ class BasicGetFieldsAction extends BasicGetAction { $this->setFieldSuffixes($field); } if (isset($defaults['options'])) { - $field['options'] = $this->formatOptionList($field['options']); + $this->formatOptionList($field); } $field = array_diff_key($field, $internalProps); } } /** - * Transforms option list into the format specified in $this->loadOptions + * Sets `options` and `suffixes` based on pseudoconstant if given. * - * @param $options - * @return array|bool + * Transforms option list into the format specified in $this->loadOptions. + * + * @param array $field */ - private function formatOptionList($options) { - if (!$this->loadOptions || !is_array($options)) { - return (bool) $options; + private function formatOptionList(&$field) { + if (empty($field['options'])) { + $field['options'] = !empty($field['pseudoconstant']); + } + if (!empty($field['pseudoconstant']['optionGroupName'])) { + $field['suffixes'] = CoreUtil::getOptionValueFields($field['pseudoconstant']['optionGroupName']); + } + if (!$this->loadOptions || !$field['options']) { + $field['options'] = (bool) $field['options']; + return; + } + if (!empty($field['pseudoconstant'])) { + if (!empty($field['pseudoconstant']['optionGroupName'])) { + $field['options'] = self::pseudoconstantOptions($field['pseudoconstant']['optionGroupName']); + } + elseif (!empty($field['pseudoconstant']['callback'])) { + $field['options'] = call_user_func(\Civi\Core\Resolver::singleton()->get($field['pseudoconstant']['callback'])); + } + else { + throw new \CRM_Core_Exception('Unsupported pseudoconstant type for field "' . $field['name'] . '"'); + } } - if (!$options) { - return $options; + if (!$field['options'] || !is_array($field['options'])) { + return; } + $formatted = []; - $first = reset($options); + $first = reset($field['options']); // Flat array requested if ($this->loadOptions === TRUE) { // Convert non-associative to flat array if (is_array($first) && isset($first['id'])) { - foreach ($options as $option) { + foreach ($field['options'] as $option) { $formatted[$option['id']] = $option['label'] ?? $option['name'] ?? $option['id']; } - return $formatted; + $field['options'] = $formatted; } - return $options; } // Non-associative array of multiple properties requested - foreach ($options as $id => $option) { - // Transform a flat list - if (!is_array($option)) { - $option = [ - 'id' => $id, - 'name' => $id, - 'label' => $option, - ]; + else { + foreach ($field['options'] as $id => $option) { + // Transform a flat list + if (!is_array($option)) { + $option = [ + 'id' => $id, + 'name' => $id, + 'label' => $option, + ]; + } + $formatted[] = array_intersect_key($option, array_flip($this->loadOptions)); } - $formatted[] = array_intersect_key($option, array_flip($this->loadOptions)); + $field['options'] = $formatted; } - return $formatted; } /** @@ -301,6 +323,10 @@ class BasicGetFieldsAction extends BasicGetAction { 'data_type' => 'Array', 'default_value' => FALSE, ], + [ + 'name' => 'pseudoconstant', + '@internal' => TRUE, + ], [ 'name' => 'suffixes', 'data_type' => 'Array', diff --git a/Civi/Api4/Service/Spec/SpecFormatter.php b/Civi/Api4/Service/Spec/SpecFormatter.php index b8bd38fc13..fb0b83a736 100644 --- a/Civi/Api4/Service/Spec/SpecFormatter.php +++ b/Civi/Api4/Service/Spec/SpecFormatter.php @@ -49,7 +49,7 @@ class SpecFormatter { $field->setOptionsCallback([__CLASS__, 'getOptions']); $suffixes = ['label']; if (!empty($data['option_group_id'])) { - $suffixes = self::getOptionValueFields($data['option_group_id'], 'id'); + $suffixes = CoreUtil::getOptionValueFields($data['option_group_id'], 'id'); } $field->setSuffixes($suffixes); } @@ -78,7 +78,7 @@ class SpecFormatter { } } if (!empty($data['pseudoconstant']['optionGroupName'])) { - $suffixes = self::getOptionValueFields($data['pseudoconstant']['optionGroupName'], 'name'); + $suffixes = CoreUtil::getOptionValueFields($data['pseudoconstant']['optionGroupName'], 'name'); } $field->setSuffixes($suffixes); } @@ -99,26 +99,6 @@ class SpecFormatter { return $field; } - /** - * Get the suffixes supported by this option group - * - * @param string|int $optionGroup - * OptionGroup id or name - * @param string $key - * Is $optionGroup being passed as "id" or "name" - * @return array - */ - private static function getOptionValueFields($optionGroup, $key) { - // Prevent crash during upgrade - if (array_key_exists('option_value_fields', \CRM_Core_DAO_OptionGroup::getSupportedFields())) { - $fields = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $optionGroup, 'option_value_fields', $key); - } - if (!isset($fields)) { - return ['name', 'label', 'description']; - } - return explode(',', $fields); - } - /** * Does this custom field have options * diff --git a/Civi/Api4/Utils/CoreUtil.php b/Civi/Api4/Utils/CoreUtil.php index 3bd5ee9b1b..f6f7801058 100644 --- a/Civi/Api4/Utils/CoreUtil.php +++ b/Civi/Api4/Utils/CoreUtil.php @@ -259,4 +259,52 @@ class CoreUtil { return $dao->getReferenceCounts(); } + /** + * @return array + */ + public static function getSearchableOptions(): array { + return [ + 'primary' => ts('Primary'), + 'secondary' => ts('Secondary'), + 'bridge' => ts('Bridge'), + 'none' => ts('None'), + ]; + } + + /** + * Collect the 'type' values from every entity. + * + * @return array + */ + public static function getEntityTypes(): array { + $provider = \Civi::service('action_object_provider'); + $entityTypes = []; + foreach ($provider->getEntities() as $entity) { + foreach ($entity['type'] ?? [] as $type) { + $entityTypes[$type] = $type; + } + } + return $entityTypes; + } + + /** + * Get the suffixes supported by a given option group + * + * @param string|int $optionGroup + * OptionGroup id or name + * @param string $key + * Is $optionGroup being passed as "id" or "name" + * @return array + */ + public static function getOptionValueFields($optionGroup, $key = 'name'): array { + // Prevent crash during upgrade + if (array_key_exists('option_value_fields', \CRM_Core_DAO_OptionGroup::getSupportedFields())) { + $fields = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $optionGroup, 'option_value_fields', $key); + } + if (!isset($fields)) { + return ['name', 'label', 'description']; + } + return explode(',', $fields); + } + } diff --git a/ext/afform/core/Civi/Api4/Afform.php b/ext/afform/core/Civi/Api4/Afform.php index 2a21cf756c..0f71813b78 100644 --- a/ext/afform/core/Civi/Api4/Afform.php +++ b/ext/afform/core/Civi/Api4/Afform.php @@ -123,8 +123,7 @@ class Afform extends Generic\AbstractEntity { ], [ 'name' => 'type', - 'options' => $self->pseudoconstantOptions('afform_type'), - 'suffixes' => ['id', 'name', 'label', 'icon'], + 'pseudoconstant' => ['optionGroupName' => 'afform_type'], ], [ 'name' => 'requires', @@ -225,7 +224,7 @@ class Afform extends Generic\AbstractEntity { 'data_type' => 'String', 'description' => 'Name of extension which provides this form', 'readonly' => TRUE, - 'options' => $self->getLoadOptions() ? \CRM_Core_PseudoConstant::getExtensions() : TRUE, + 'pseudoconstant' => ['callback' => ['CRM_Core_PseudoConstant', 'getExtensions']], ]; $fields[] = [ 'name' => 'search_displays', diff --git a/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetFieldsTest.php b/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetFieldsTest.php new file mode 100644 index 0000000000..b42cd4af4c --- /dev/null +++ b/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetFieldsTest.php @@ -0,0 +1,27 @@ +installMe(__DIR__)->apply(); + } + + public function testGetFields() { + $fields = Afform::getFields(FALSE) + ->setAction('get') + ->execute()->indexBy('name'); + $this->assertTrue($fields['type']['options']); + $this->assertEquals(['name', 'label', 'icon', 'description'], $fields['type']['suffixes']); + + $this->assertTrue($fields['base_module']['options']); + $this->assertTrue($fields['contact_summary']['options']); + } + +} -- 2.25.1