APIv4 - Improve pseudoconstant support in getFields
authorColeman Watts <coleman@civicrm.org>
Sun, 31 Jul 2022 01:41:17 +0000 (21:41 -0400)
committerColeman Watts <coleman@civicrm.org>
Sun, 31 Jul 2022 14:18:47 +0000 (10:18 -0400)
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
Civi/Api4/Generic/BasicGetFieldsAction.php
Civi/Api4/Service/Spec/SpecFormatter.php
Civi/Api4/Utils/CoreUtil.php
ext/afform/core/Civi/Api4/Afform.php
ext/afform/core/tests/phpunit/Civi/Afform/AfformGetFieldsTest.php [new file with mode: 0644]

index 3d362edfe930e9bc4bec7fcb5002561152cb9c97..a53248cf3ac8c0e752210bb0dba40a46639e1a2f 100644 (file)
@@ -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;
-  }
-
 }
index 494af4829cd44341f42e73e02ee646e6f485ac9f..36452822316be7824fcfa85bb9ee2cd54489e1f7 100644 (file)
@@ -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',
index b8bd38fc13ec2c2e72151a38a70d2a054b056990..fb0b83a7360e419fec7b1fefe31bde3c37e0509b 100644 (file)
@@ -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
    *
index 3bd5ee9b1b0dc3225e6360f4b78e718f429f368b..f6f7801058b3e1ce1f533274da7539ca1ca783a5 100644 (file)
@@ -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);
+  }
+
 }
index 2a21cf756cb00912f433063abd75740366ca1b1c..0f71813b78497a4ac8111e8bf5109917a8f23ec2 100644 (file)
@@ -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 (file)
index 0000000..b42cd4a
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+namespace Civi\Afform;
+
+use Civi\Api4\Afform;
+use Civi\Test\HeadlessInterface;
+
+/**
+ * @group headless
+ */
+class AfformGetFieldsTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface {
+
+  public function setUpHeadless() {
+    return \Civi\Test::headless()->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']);
+  }
+
+}