diff --git a/api/v3/utils.php b/api/v3/utils.php index f0db7003173a..51391dc2b334 100644 --- a/api/v3/utils.php +++ b/api/v3/utils.php @@ -2472,12 +2472,82 @@ 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 = []; + $filterCompare = function($value, $operator, $expected) { + switch ($operator) { + case '=': + case '!=': + case '<>': + return ($value == $expected) == ($operator == '='); + + case 'IS NULL': + case 'IS NOT NULL': + return is_null($value) == ($operator == 'IS NULL'); + + case '>': + return $value > $expected; + + case '>=': + return $value >= $expected; + + case '<': + return $value < $expected; + + case '<=': + return $value <= $expected; + + case 'BETWEEN': + case 'NOT BETWEEN': + $between = ($value >= $expected[0] && $value <= $expected[1]); + return $between == ($operator == 'BETWEEN'); + + case 'LIKE': + case 'NOT LIKE': + $pattern = '/^' . str_replace('%', '.*', preg_quote($expected, '/')) . '$/i'; + return !preg_match($pattern, $value) == ($operator != 'LIKE'); + + case 'IN': + case 'NOT IN': + return in_array($value, $expected) == ($operator == 'IN'); + + default: + throw new API_Exception("Unsupported operator: '$operator' cannot be used with array data"); + } + }; + + $isMatch = function($recordVal, $searchValue) use ($filterCompare) { + $operator = '='; + if (is_array($searchValue) && count($searchValue) === 1 && in_array(array_keys($searchValue)[0], CRM_Core_DAO::acceptedSQLOperators())) { + $operator = array_keys($searchValue)[0]; + $searchValue = array_values($searchValue)[0]; + } + return $filterCompare($recordVal, $operator, $searchValue); + }; + $currentOffset = 0; foreach ($records as $record) { if ($idCol != 'id') { @@ -2488,7 +2558,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 f472f3c0b778..3e1d5b27f5cb 100644 --- a/tests/phpunit/api/v3/UtilsTest.php +++ b/tests/phpunit/api/v3/UtilsTest.php @@ -376,16 +376,46 @@ public function basicArrayCases() { $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 @@ public function testBasicArrayGet($records, $params, $resultIds) { $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'])));