Api4 - format output consistently across get/create/update.
authorColeman Watts <coleman@civicrm.org>
Thu, 16 Jan 2020 16:58:22 +0000 (11:58 -0500)
committerColeman Watts <coleman@civicrm.org>
Thu, 16 Jan 2020 18:38:26 +0000 (13:38 -0500)
Previously output from a DAO-based Get would be run through an unserializer/type-converter.
This applies that same conversion to DAO-based create/update ops.

Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php
Civi/Api4/Generic/Traits/DAOActionTrait.php
Civi/Api4/Query/Api4SelectQuery.php
Civi/Api4/Utils/FormattingUtil.php
ang/api4Explorer/Explorer.js
tests/phpunit/api/v3/UFGroupTest.php

index 6d7cd949f0daf8652004d0a4c2618c8bf62e2a4f..00b2cc943997141c6175c052ea744e73e070ec99 100644 (file)
@@ -25,6 +25,7 @@ use Civi\Api4\Event\Events;
 use Civi\Api4\Event\PostSelectQueryEvent;
 use Civi\Api4\Query\Api4SelectQuery;
 use Civi\Api4\Utils\ArrayInsertionUtil;
+use Civi\Api4\Utils\FormattingUtil;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -32,8 +33,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 class PostSelectQuerySubscriber implements EventSubscriberInterface {
 
-  private $contactFieldsToRemove = [];
-
   /**
    * @inheritdoc
    */
@@ -62,7 +61,7 @@ class PostSelectQuerySubscriber implements EventSubscriberInterface {
       return $results;
     }
 
-    $this->formatFieldValues($results, $query->getApiFieldSpec(), $query->getEntity());
+    FormattingUtil::formatOutputValues($results, $query->getApiFieldSpec(), $query->getEntity());
 
     // Group the selects to avoid queries for each field
     $groupedSelects = $this->getNtoManyJoinSelects($query);
@@ -99,60 +98,8 @@ class PostSelectQuerySubscriber implements EventSubscriberInterface {
       $fields[array_pop($name)] = $field->toArray();
     }
     if ($fields) {
-      $this->formatFieldValues($joinResults, $fields, $join->getEntity());
-    }
-  }
-
-  /**
-   * Unserialize values and convert to correct type
-   *
-   * @param array $results
-   * @param array $fields
-   * @param string $entity
-   */
-  protected function formatFieldValues(&$results, $fields, $entity) {
-    foreach ($results as &$result) {
-      // Remove inapplicable contact fields
-      if ($entity === 'Contact' && !empty($result['contact_type'])) {
-        \CRM_Utils_Array::remove($result, $this->contactFieldsToRemove($result['contact_type']));
-      }
-      foreach ($result as $field => $value) {
-        $dataType = $fields[$field]['data_type'] ?? 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] = $this->convertDataType($val, $dataType);
-            }
-          }
-        }
-        else {
-          $result[$field] = $this->convertDataType($value, $dataType);
-        }
-      }
-    }
-  }
-
-  /**
-   * @param mixed $value
-   * @param string $dataType
-   * @return mixed
-   */
-  protected function convertDataType($value, $dataType) {
-    if (isset($value)) {
-      switch ($dataType) {
-        case 'Boolean':
-          return (bool) $value;
-
-        case 'Integer':
-          return (int) $value;
-
-        case 'Money':
-        case 'Float':
-          return (float) $value;
-      }
+      FormattingUtil::formatOutputValues($joinResults, $fields, $join->getEntity());
     }
-    return $value;
   }
 
   /**
@@ -382,20 +329,4 @@ class PostSelectQuerySubscriber implements EventSubscriberInterface {
     return $subResults;
   }
 
-  /**
-   * @param string $contactType
-   * @return array
-   */
-  private function contactFieldsToRemove($contactType) {
-    if (!isset($this->contactFieldsToRemove[$contactType])) {
-      $this->contactFieldsToRemove[$contactType] = [];
-      foreach (\CRM_Contact_DAO_Contact::fields() as $field) {
-        if (!empty($field['contactType']) && $field['contactType'] != $contactType) {
-          $this->contactFieldsToRemove[$contactType][] = $field['name'];
-        }
-      }
-    }
-    return $this->contactFieldsToRemove[$contactType];
-  }
-
 }
index 3748daa4ab3456babec321adb622343cd94825bd..941056a050f5267aaa974083948bafde3d182144 100644 (file)
@@ -20,7 +20,6 @@
 
 namespace Civi\Api4\Generic\Traits;
 
-use CRM_Utils_Array as UtilsArray;
 use Civi\Api4\Utils\FormattingUtil;
 use Civi\Api4\Query\Api4SelectQuery;
 
@@ -129,7 +128,7 @@ trait DAOActionTrait {
     $result = [];
 
     foreach ($items as $item) {
-      $entityId = UtilsArray::value('id', $item);
+      $entityId = $item['id'] ?? NULL;
       FormattingUtil::formatWriteParams($item, $this->getEntityName(), $this->entityFields());
       $this->formatCustomParams($item, $entityId);
       $item['check_permissions'] = $this->getCheckPermissions();
@@ -167,6 +166,7 @@ trait DAOActionTrait {
 
       $result[] = $resultArray;
     }
+    FormattingUtil::formatOutputValues($result, $this->entityFields(), $this->getEntityName());
     return $result;
   }
 
@@ -180,7 +180,7 @@ trait DAOActionTrait {
     $baoName = $this->getBaoName();
     $hook = empty($params['id']) ? 'create' : 'edit';
 
-    \CRM_Utils_Hook::pre($hook, $this->getEntityName(), UtilsArray::value('id', $params), $params);
+    \CRM_Utils_Hook::pre($hook, $this->getEntityName(), $params['id'] ?? NULL, $params);
     /** @var \CRM_Core_DAO $instance */
     $instance = new $baoName();
     $instance->copyValues($params, TRUE);
index 80ecaf55cf046aabdc7cd35c7c7312d9c658514d..3fb86d3607f3d221cd90bf79d42cbec63ebcc6e2 100644 (file)
@@ -280,7 +280,7 @@ class Api4SelectQuery extends SelectQuery {
       throw new \API_Exception("Invalid field '$key' in where clause.");
     }
 
-    FormattingUtil::formatValue($value, $fieldSpec, $this->getEntity());
+    FormattingUtil::formatInputValue($value, $fieldSpec, $this->getEntity());
 
     $sql_clause = \CRM_Core_DAO::createSQLFilter("`$table_name`.`$column_name`", [$operator => $value]);
     if ($sql_clause === NULL) {
index 21c3d4e6b59de29757925c3a5b3d86c601104b6d..b4a55d493ce2e458f2e901ceef3cc17e2a16323b 100644 (file)
@@ -21,8 +21,6 @@
 
 namespace Civi\Api4\Utils;
 
-use CRM_Utils_Array as UtilsArray;
-
 require_once 'api/v3/utils.php';
 
 class FormattingUtil {
@@ -43,7 +41,7 @@ class FormattingUtil {
         if ($value === 'null') {
           $value = 'Null';
         }
-        FormattingUtil::formatValue($value, $field, $entity);
+        self::formatInputValue($value, $field, $entity);
         // Ensure we have an array for serialized fields
         if (!empty($field['serialize'] && !is_array($value))) {
           $value = (array) $value;
@@ -80,18 +78,14 @@ class FormattingUtil {
    *   Ex: 'Contact', 'Domain'
    * @throws \API_Exception
    */
-  public static function formatValue(&$value, $fieldSpec, $entity) {
+  public static function formatInputValue(&$value, $fieldSpec, $entity) {
     if (is_array($value)) {
       foreach ($value as &$val) {
-        self::formatValue($val, $fieldSpec, $entity);
+        self::formatInputValue($val, $fieldSpec, $entity);
       }
       return;
     }
-    $fk = UtilsArray::value('fk_entity', $fieldSpec);
-    if ($fieldSpec['name'] == 'id') {
-      $fk = $entity;
-    }
-    $dataType = UtilsArray::value('data_type', $fieldSpec);
+    $fk = $fieldSpec['name'] == 'id' ? $entity : $fieldSpec['fk_entity'] ?? NULL;
 
     if ($fk === 'Domain' && $value === 'current_domain') {
       $value = \CRM_Core_Config::domainID();
@@ -104,7 +98,7 @@ class FormattingUtil {
       }
     }
 
-    switch ($dataType) {
+    switch ($fieldSpec['data_type'] ?? NULL) {
       case 'Timestamp':
         $value = date('Y-m-d H:i:s', strtotime($value));
         break;
@@ -120,4 +114,73 @@ class FormattingUtil {
     }
   }
 
+  /**
+   * Unserialize raw DAO values and convert to correct type
+   *
+   * @param array $results
+   * @param array $fields
+   * @param string $entity
+   * @throws \CRM_Core_Exception
+   */
+  public static function formatOutputValues(&$results, $fields, $entity) {
+    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);
+            }
+          }
+        }
+        else {
+          $result[$field] = self::convertDataType($value, $dataType);
+        }
+      }
+    }
+  }
+
+  /**
+   * @param mixed $value
+   * @param string $dataType
+   * @return mixed
+   */
+  public static function convertDataType($value, $dataType) {
+    if (isset($value)) {
+      switch ($dataType) {
+        case 'Boolean':
+          return (bool) $value;
+
+        case 'Integer':
+          return (int) $value;
+
+        case 'Money':
+        case 'Float':
+          return (float) $value;
+      }
+    }
+    return $value;
+  }
+
+  /**
+   * @param string $contactType
+   * @return array
+   */
+  public static function contactFieldsToRemove($contactType) {
+    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'];
+        }
+      }
+    }
+    return \Civi::$statics[__CLASS__][__FUNCTION__][$contactType];
+  }
+
 }
index 2bfc82c9e4a1d79d7cf2e579106de18853312c0a..cd5d61a2d9a4d6defe343e62eb9ee5f60eb4b1dd 100644 (file)
               });
             } else if (dataType === 'Boolean') {
               $el.attr('placeholder', ts('- select -')).crmSelect2({allowClear: false, multiple: multi, placeholder: ts('- select -'), data: [
-                {id: '1', text: ts('Yes')},
-                {id: '0', text: ts('No')}
+                {id: 'true', text: ts('Yes')},
+                {id: 'false', text: ts('No')}
               ]});
             }
           } else if (dataType === 'Integer' && !multi) {
index 8d10ba5cf3003e8ce538d963af42d23509a48ef4..be06e412fe584f8d837df1bd0c0e63cbdfe8a5e0 100644 (file)
@@ -115,8 +115,11 @@ class api_v3_UFGroupTest extends CiviUnitTestCase {
       if ($key == 'add_contact_to_group' or $key == 'group') {
         continue;
       }
-      $expected = $this->params[$key];
       $received = $result['values'][$result['id']][$key];
+      if ($key == 'group_type' && $version == 4) {
+        $received = implode(',', $received);
+      }
+      $expected = $this->params[$key];
       $this->assertEquals($expected, $received, "The string '$received' does not equal '$expected' for key '$key' in line " . __LINE__);
     }
   }
@@ -165,7 +168,11 @@ class api_v3_UFGroupTest extends CiviUnitTestCase {
       if ($key == 'add_contact_to_group' or $key == 'group') {
         continue;
       }
-      $this->assertEquals($result['values'][$result['id']][$key], $params[$key], $key . " doesn't match  " . $value);
+      $received = $result['values'][$result['id']][$key];
+      if ($key == 'group_type' && $version == 4) {
+        $received = implode(',', $received);
+      }
+      $this->assertEquals($received, $params[$key], $key . " doesn't match  " . $value);
     }
 
     $this->assertEquals($result['values'][$this->_ufGroupId]['add_to_group_id'], $params['add_contact_to_group']);