APIv4 - Add explicit joins
[civicrm-core.git] / Civi / Api4 / Utils / FormattingUtil.php
index b4a55d493ce2e458f2e901ceef3cc17e2a16323b..b3cd5ecd0528b4d203dee505aae2fd2de4a42ab5 100644 (file)
@@ -25,15 +25,20 @@ require_once 'api/v3/utils.php';
 
 class FormattingUtil {
 
+  public static $pseudoConstantContexts = [
+    'name' => 'validate',
+    'abbr' => 'abbreviate',
+    'label' => 'get',
+  ];
+
   /**
    * Massage values into the format the BAO expects for a write operation
    *
-   * @param $params
-   * @param $entity
-   * @param $fields
+   * @param array $params
+   * @param array $fields
    * @throws \API_Exception
    */
-  public static function formatWriteParams(&$params, $entity, $fields) {
+  public static function formatWriteParams(&$params, $fields) {
     foreach ($fields as $name => $field) {
       if (!empty($params[$name])) {
         $value =& $params[$name];
@@ -41,7 +46,7 @@ class FormattingUtil {
         if ($value === 'null') {
           $value = 'Null';
         }
-        self::formatInputValue($value, $field, $entity);
+        self::formatInputValue($value, $name, $field);
         // Ensure we have an array for serialized fields
         if (!empty($field['serialize'] && !is_array($value))) {
           $value = (array) $value;
@@ -73,19 +78,25 @@ class FormattingUtil {
    * This is used by read AND write actions (Get, Create, Update, Replace)
    *
    * @param $value
-   * @param $fieldSpec
-   * @param string $entity
-   *   Ex: 'Contact', 'Domain'
+   * @param string $fieldName
+   * @param array $fieldSpec
    * @throws \API_Exception
    */
-  public static function formatInputValue(&$value, $fieldSpec, $entity) {
-    if (is_array($value)) {
+  public static function formatInputValue(&$value, $fieldName, $fieldSpec) {
+    // Evaluate pseudoconstant suffix
+    $suffix = strpos($fieldName, ':');
+    if ($suffix) {
+      $options = self::getPseudoconstantList($fieldSpec['entity'], $fieldSpec['name'], substr($fieldName, $suffix + 1));
+      $value = self::replacePseudoconstant($options, $value, TRUE);
+      return;
+    }
+    elseif (is_array($value)) {
       foreach ($value as &$val) {
-        self::formatInputValue($val, $fieldSpec, $entity);
+        self::formatInputValue($val, $fieldName, $fieldSpec);
       }
       return;
     }
-    $fk = $fieldSpec['name'] == 'id' ? $entity : $fieldSpec['fk_entity'] ?? NULL;
+    $fk = $fieldSpec['name'] == 'id' ? $fieldSpec['entity'] : $fieldSpec['fk_entity'] ?? NULL;
 
     if ($fk === 'Domain' && $value === 'current_domain') {
       $value = \CRM_Core_Config::domainID();
@@ -120,38 +131,121 @@ class FormattingUtil {
    * @param array $results
    * @param array $fields
    * @param string $entity
+   * @param string $action
+   * @throws \API_Exception
    * @throws \CRM_Core_Exception
    */
-  public static function formatOutputValues(&$results, $fields, $entity) {
+  public static function formatOutputValues(&$results, $fields, $entity, $action = 'get') {
+    $fieldOptions = [];
     foreach ($results as &$result) {
-      // Remove inapplicable contact fields
-      if ($entity === 'Contact' && !empty($result['contact_type'])) {
-        \CRM_Utils_Array::remove($result, self::contactFieldsToRemove($result['contact_type']));
-      }
-      foreach ($result as $field => $value) {
-        $dataType = $fields[$field]['data_type'] ?? ($field == 'id' ? 'Integer' : NULL);
-        if (!empty($fields[$field]['serialize'])) {
-          if (is_string($value)) {
-            $result[$field] = $value = \CRM_Core_DAO::unSerializeField($value, $fields[$field]['serialize']);
-            foreach ($value as $key => $val) {
-              $result[$field][$key] = self::convertDataType($val, $dataType);
+      $contactTypePaths = [];
+      foreach ($result as $fieldExpr => $value) {
+        $field = $fields[$fieldExpr] ?? NULL;
+        $dataType = $field['data_type'] ?? ($fieldExpr == 'id' ? 'Integer' : NULL);
+        if ($field) {
+          // Evaluate pseudoconstant suffixes
+          $suffix = strrpos($fieldExpr, ':');
+          if ($suffix) {
+            $fieldName = empty($field['custom_field_id']) ? $field['name'] : 'custom_' . $field['custom_field_id'];
+            $fieldOptions[$fieldExpr] = $fieldOptions[$fieldExpr] ?? self::getPseudoconstantList($field['entity'], $fieldName, substr($fieldExpr, $suffix + 1), $result, $action);
+            $dataType = NULL;
+          }
+          if (!empty($field['serialize'])) {
+            if (is_string($value)) {
+              $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']);
             }
           }
+          if (isset($fieldOptions[$fieldExpr])) {
+            $value = self::replacePseudoconstant($fieldOptions[$fieldExpr], $value);
+          }
+          // Keep track of contact types for self::contactFieldsToRemove
+          if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') {
+            $prefix = strrpos($fieldExpr, '.');
+            $contactTypePaths[$prefix ? substr($fieldExpr, 0, $prefix + 1) : ''] = $value;
+          }
         }
-        else {
-          $result[$field] = self::convertDataType($value, $dataType);
-        }
+        $result[$fieldExpr] = self::convertDataType($value, $dataType);
+      }
+      // Remove inapplicable contact fields
+      foreach ($contactTypePaths as $prefix => $contactType) {
+        \CRM_Utils_Array::remove($result, self::contactFieldsToRemove($contactType, $prefix));
       }
     }
   }
 
+  /**
+   * Retrieves pseudoconstant option list for a field.
+   *
+   * @param string $entity
+   *   Name of api entity
+   * @param string $fieldName
+   * @param string $valueType
+   *   name|label|abbr from self::$pseudoConstantContexts
+   * @param array $params
+   *   Other values for this object
+   * @param string $action
+   * @return array
+   * @throws \API_Exception
+   */
+  public static function getPseudoconstantList($entity, $fieldName, $valueType, $params = [], $action = 'get') {
+    $context = self::$pseudoConstantContexts[$valueType] ?? NULL;
+    if (!$context) {
+      throw new \API_Exception('Illegal expression');
+    }
+    $baoName = CoreUtil::getBAOFromApiName($entity);
+    // 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
+    if (!isset($options) || $options === FALSE) {
+      $options = civicrm_api4($entity, 'getFields', ['action' => $action, 'loadOptions' => TRUE, 'where' => [['name', '=', $fieldName]]])[0]['options'] ?? NULL;
+    }
+    if (is_array($options)) {
+      return $options;
+    }
+    throw new \API_Exception("No option list found for '$fieldName'");
+  }
+
+  /**
+   * Replaces value (or an array of values) with options from a pseudoconstant list.
+   *
+   * The direction of lookup defaults to transforming ids to option values for api output;
+   * for api input, set $reverse = TRUE to transform option values to ids.
+   *
+   * @param array $options
+   * @param string|string[] $value
+   * @param bool $reverse
+   *   Is this a reverse lookup (for transforming input instead of output)
+   * @return array|mixed|null
+   */
+  public static function replacePseudoconstant($options, $value, $reverse = FALSE) {
+    $matches = [];
+    foreach ((array) $value as $val) {
+      if (!$reverse && isset($options[$val])) {
+        $matches[] = $options[$val];
+      }
+      elseif ($reverse && array_search($val, $options) !== FALSE) {
+        $matches[] = array_search($val, $options);
+      }
+    }
+    return is_array($value) ? $matches : $matches[0] ?? NULL;
+  }
+
   /**
    * @param mixed $value
    * @param string $dataType
    * @return mixed
    */
   public static function convertDataType($value, $dataType) {
-    if (isset($value)) {
+    if (isset($value) && $dataType) {
+      if (is_array($value)) {
+        foreach ($value as $key => $val) {
+          $value[$key] = self::convertDataType($val, $dataType);
+        }
+        return $value;
+      }
+
       switch ($dataType) {
         case 'Boolean':
           return (bool) $value;
@@ -168,19 +262,33 @@ class FormattingUtil {
   }
 
   /**
+   * Lists all field names (including suffixed variants) that should be removed for a given contact type.
+   *
    * @param string $contactType
+   *   Individual|Organization|Household
+   * @param string $prefix
+   *   Path at which these fields are found, e.g. "address.contact."
    * @return array
    */
-  public static function contactFieldsToRemove($contactType) {
+  public static function contactFieldsToRemove($contactType, $prefix) {
     if (!isset(\Civi::$statics[__CLASS__][__FUNCTION__][$contactType])) {
       \Civi::$statics[__CLASS__][__FUNCTION__][$contactType] = [];
       foreach (\CRM_Contact_DAO_Contact::fields() as $field) {
         if (!empty($field['contactType']) && $field['contactType'] != $contactType) {
           \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) {
+              \Civi::$statics[__CLASS__][__FUNCTION__][$contactType][] = $field['name'] . ':' . $suffix;
+            }
+          }
         }
       }
     }
-    return \Civi::$statics[__CLASS__][__FUNCTION__][$contactType];
+    // Add prefix paths
+    return array_map(function($name) use ($prefix) {
+      return $prefix . $name;
+    }, \Civi::$statics[__CLASS__][__FUNCTION__][$contactType]);
   }
 
 }