APIv3 - Improve array-based apis to support sorting and multiple operators
authorColeman Watts <coleman@civicrm.org>
Sun, 28 Feb 2021 04:18:41 +0000 (23:18 -0500)
committerColeman Watts <coleman@civicrm.org>
Mon, 1 Mar 2021 01:10:29 +0000 (20:10 -0500)
This backports some APIv4 code to v3, for the purpose of supporting
entityRef widgets for Afform.

api/v3/utils.php
tests/phpunit/api/v3/UtilsTest.php

index f0db7003173a142af4fd19586453cf757e99f5cf..88237f41f0a53e1d6911e84bea9648e84b5cd3be 100644 (file)
@@ -2472,12 +2472,78 @@ function _civicrm_api3_field_value_check(&$params, $fieldName, $type = NULL) {
  */
 function _civicrm_api3_basic_array_get($entity, $params, $records, $idCol, $filterableFields) {
   $options = _civicrm_api3_get_options_from_params($params, TRUE, $entity, 'get');
-  // TODO // $sort = CRM_Utils_Array::value('sort', $options, NULL);
   $offset = $options['offset'] ?? NULL;
   $limit = $options['limit'] ?? NULL;
 
+  $sort = !empty($options['sort']) ? explode(', ', $options['sort']) : NULL;
+  if ($sort) {
+    usort($records, function($a, $b) use ($sort) {
+      foreach ($sort as $field) {
+        [$field, $dir] = array_pad(explode(' ', $field), 2, 'asc');
+        $modifier = strtolower($dir) == 'asc' ? 1 : -1;
+        if (isset($a[$field]) && isset($b[$field])) {
+          if ($a[$field] == $b[$field]) {
+            continue;
+          }
+          return (strnatcasecmp($a[$field], $b[$field]) * $modifier);
+        }
+        elseif (isset($a[$field]) || isset($b[$field])) {
+          return ((isset($a[$field]) ? 1 : -1) * $modifier);
+        }
+      }
+      return 0;
+    });
+  }
+
   $matches = [];
 
+  $isMatch = function($recordVal, $searchVal) {
+    $operator = '=';
+    if (is_array($searchVal) && count($searchVal) === 1 && in_array(array_keys($searchVal)[0], CRM_Core_DAO::acceptedSQLOperators())) {
+      $operator = array_keys($searchVal)[0];
+      $searchVal = array_values($searchVal)[0];
+    }
+    switch ($operator) {
+      case '=':
+      case '!=':
+      case '<>':
+        return ($recordVal == $searchVal) == ($operator == '=');
+
+      case 'IS NULL':
+      case 'IS NOT NULL':
+        return is_null($recordVal) == ($operator == 'IS NULL');
+
+      case '>':
+        return $recordVal > $searchVal;
+
+      case '>=':
+        return $recordVal >= $searchVal;
+
+      case '<':
+        return $recordVal < $searchVal;
+
+      case '<=':
+        return $recordVal <= $searchVal;
+
+      case 'BETWEEN':
+      case 'NOT BETWEEN':
+        $between = ($recordVal >= $searchVal[0] && $recordVal <= $searchVal[1]);
+        return $between == ($operator == 'BETWEEN');
+
+      case 'LIKE':
+      case 'NOT LIKE':
+        $pattern = '/^' . str_replace('%', '.*', preg_quote($searchVal, '/')) . '$/i';
+        return !preg_match($pattern, $recordVal) == ($operator != 'LIKE');
+
+      case 'IN':
+      case 'NOT IN':
+        return in_array($recordVal, $searchVal) == ($operator == 'IN');
+
+      default:
+        throw new API_Exception("Unsupported operator: '$operator' cannot be used with array data");
+    }
+  };
+
   $currentOffset = 0;
   foreach ($records as $record) {
     if ($idCol != 'id') {
@@ -2488,7 +2554,7 @@ function _civicrm_api3_basic_array_get($entity, $params, $records, $idCol, $filt
       if ($k == 'id') {
         $k = $idCol;
       }
-      if (in_array($k, $filterableFields) && $record[$k] != $v) {
+      if (in_array($k, $filterableFields) && !$isMatch($record[$k] ?? NULL, $v)) {
         $match = FALSE;
         break;
       }
index f472f3c0b77858baac738efd4927ec3b18713079..3e1d5b27f5cba1e38a98ae1d02e761aefe147936 100644 (file)
@@ -376,16 +376,46 @@ class api_v3_UtilsTest extends CiviUnitTestCase {
 
     $cases[] = [
       $records,
-      ['version' => 3, 'cheese' => 'cheddar'],
+      ['version' => 3, 'cheese' => 'cheddar', 'options' => ['sort' => 'fruit desc']],
       ['b', 'c'],
     ];
 
+    $cases[] = [
+      $records,
+      ['version' => 3, 'cheese' => 'cheddar', 'options' => ['sort' => 'fruit']],
+      ['c', 'b'],
+    ];
+
+    $cases[] = [
+      $records,
+      ['version' => 3, 'cheese' => ['IS NOT NULL' => 1], 'options' => ['sort' => 'fruit, cheese']],
+      ['c', 'd', 'e', 'a', 'b'],
+    ];
+
     $cases[] = [
       $records,
       ['version' => 3, 'id' => 'd'],
       ['d'],
     ];
 
+    $cases[] = [
+      $records,
+      ['version' => 3, 'fruit' => ['!=' => 'apple']],
+      ['b'],
+    ];
+
+    $cases[] = [
+      $records,
+      ['version' => 3, 'cheese' => ['LIKE' => '%o%']],
+      ['d', 'e'],
+    ];
+
+    $cases[] = [
+      $records,
+      ['version' => 3, 'cheese' => ['IN' => ['swiss', 'cheddar', 'gouda']]],
+      ['a', 'b', 'c', 'd'],
+    ];
+
     return $cases;
   }
 
@@ -423,7 +453,9 @@ class api_v3_UtilsTest extends CiviUnitTestCase {
     $this->assertEquals($resultIds, array_values(CRM_Utils_Array::collect('snack_id', $r2['values'])));
     $this->assertEquals($resultIds, array_values(CRM_Utils_Array::collect('id', $r2['values'])));
 
-    $r3 = $kernel->runSafe('Widget', 'get', $params + ['options' => ['offset' => 1, 'limit' => 2]]);
+    $params['options']['offset'] = 1;
+    $params['options']['limit'] = 2;
+    $r3 = $kernel->runSafe('Widget', 'get', $params);
     $slice = array_slice($resultIds, 1, 2);
     $this->assertEquals(count($slice), $r3['count']);
     $this->assertEquals($slice, array_values(CRM_Utils_Array::collect('snack_id', $r3['values'])));