APIv4 - Enable getFields to find fields across implicit FK joins
authorColeman Watts <coleman@civicrm.org>
Wed, 3 Feb 2021 16:48:26 +0000 (11:48 -0500)
committerColeman Watts <coleman@civicrm.org>
Wed, 3 Feb 2021 16:48:26 +0000 (11:48 -0500)
Now it is possible to retrieve field metadata for a joined entity

Civi/Api4/Generic/DAOGetFieldsAction.php
Civi/Api4/Service/Spec/RequestSpec.php
tests/phpunit/api/v4/Action/GetExtraFieldsTest.php

index 631e91652f025f3dcb7d2206f8aa9a65932f7f31..1ad7c3d3fb71d42d2bba1c7a448bec7a3c029c98 100644 (file)
@@ -40,15 +40,48 @@ class DAOGetFieldsAction extends BasicGetFieldsAction {
    * @return array
    */
   protected function getRecords() {
-    $fields = $this->_itemsToGet('name');
+    $fieldsToGet = $this->_itemsToGet('name');
     /** @var \Civi\Api4\Service\Spec\SpecGatherer $gatherer */
     $gatherer = \Civi::container()->get('spec_gatherer');
-    // Any fields name with a dot in it is custom
-    if ($fields) {
-      $this->includeCustom = strpos(implode('', $fields), '.') !== FALSE;
+    // Any fields name with a dot in it is either custom or an implicit join
+    if ($fieldsToGet) {
+      $this->includeCustom = strpos(implode('', $fieldsToGet), '.') !== FALSE;
     }
     $spec = $gatherer->getSpec($this->getEntityName(), $this->getAction(), $this->includeCustom, $this->values);
-    return SpecFormatter::specToArray($spec->getFields($fields), $this->loadOptions, $this->values);
+    $fields = SpecFormatter::specToArray($spec->getFields($fieldsToGet), $this->loadOptions, $this->values);
+    foreach ($fieldsToGet ?? [] as $fieldName) {
+      if (empty($fields[$fieldName]) && strpos($fieldName, '.') !== FALSE) {
+        $fkField = $this->getFkFieldSpec($fieldName, $fields);
+        if ($fkField) {
+          $fkField['name'] = $fieldName;
+          $fields[] = $fkField;
+        }
+      }
+    }
+    return $fields;
+  }
+
+  /**
+   * @param string $fieldName
+   * @param array $fields
+   * @return array|null
+   * @throws \API_Exception
+   */
+  private function getFkFieldSpec($fieldName, $fields) {
+    $fieldPath = explode('.', $fieldName);
+    // Search for the first segment alone plus the first and second
+    // No field in the schema contains more than one dot in its name.
+    $searchPaths = [$fieldPath[0], $fieldPath[0] . '.' . $fieldPath[1]];
+    $fkFieldName = array_intersect($searchPaths, array_keys($fields))[0] ?? NULL;
+    if ($fkFieldName && !empty($fields[$fkFieldName]['fk_entity'])) {
+      $newFieldName = substr($fieldName, 1 + strlen($fkFieldName));
+      return civicrm_api4($fields[$fkFieldName]['fk_entity'], 'getFields', [
+        'checkPermissions' => $this->checkPermissions,
+        'where' => [['name', '=', $newFieldName]],
+        'loadOptions' => $this->loadOptions,
+        'action' => $this->action,
+      ])->first();
+    }
   }
 
   public function fields() {
index f75a1f9e1c52ba27992f2d5b214d4f9b39bfc630..dbf460a3ba71bd2888ce0d435b0a5608cdded360 100644 (file)
@@ -101,13 +101,15 @@ class RequestSpec {
     if (!$fieldNames) {
       return $this->fields;
     }
-    $fields = [];
-    foreach ($this->fields as $field) {
-      if (in_array($field->getName(), $fieldNames)) {
-        $fields[] = $field;
+    // Return all exact matches plus partial matches (to support retrieving fk fields)
+    return array_filter($this->fields, function($field) use($fieldNames) {
+      foreach ($fieldNames as $fieldName) {
+        if (strpos($fieldName, $field->getName()) === 0) {
+          return TRUE;
+        }
       }
-    }
-    return $fields;
+      return FALSE;
+    });
   }
 
   /**
index 0ac148059d2a03d7c883297df59862b5da311d47..af4fe7d3ee1a22048203eca8e2c7e311dba2912d 100644 (file)
@@ -64,4 +64,18 @@ class GetExtraFieldsTest extends UnitTestCase {
     $this->assertContains('Alberta', $caOptions['options']);
   }
 
+  public function testGetFkFields() {
+    $fields = \Civi\Api4\Participant::getFields()
+      ->setLoadOptions(TRUE)
+      ->addWhere('name', 'IN', ['event_id', 'event_id.created_id', 'contact_id.gender_id', 'event_id.created_id.sort_name'])
+      ->execute()
+      ->indexBy('name');
+
+    $this->assertCount(4, $fields);
+    $this->assertEquals('Participant', $fields['event_id']['entity']);
+    $this->assertEquals('Event', $fields['event_id.created_id']['entity']);
+    $this->assertEquals('Contact', $fields['event_id.created_id.sort_name']['entity']);
+    $this->assertGreaterThan(1, count($fields['contact_id.gender_id']['options']));
+  }
+
 }