From: Coleman Watts Date: Sun, 28 Feb 2021 04:18:41 +0000 (-0500) Subject: APIv3 - Improve array-based apis to support sorting and multiple operators X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=385216e9361ffd2a198f9e416419b116ed6a180d;p=civicrm-core.git APIv3 - Improve array-based apis to support sorting and multiple operators This backports some APIv4 code to v3, for the purpose of supporting entityRef widgets for Afform. --- diff --git a/api/v3/utils.php b/api/v3/utils.php index f0db700317..88237f41f0 100644 --- a/api/v3/utils.php +++ b/api/v3/utils.php @@ -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; } diff --git a/tests/phpunit/api/v3/UtilsTest.php b/tests/phpunit/api/v3/UtilsTest.php index f472f3c0b7..3e1d5b27f5 100644 --- a/tests/phpunit/api/v3/UtilsTest.php +++ b/tests/phpunit/api/v3/UtilsTest.php @@ -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'])));