Add is empty filter to search / api
[civicrm-core.git] / Civi / Api4 / Generic / Traits / ArrayQueryActionTrait.php
1 <?php
2
3 /*
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
6 | |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
11 */
12
13 /**
14 *
15 * @package CRM
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 */
18
19
20 namespace Civi\Api4\Generic\Traits;
21
22 use Civi\API\Exception\NotImplementedException;
23
24 /**
25 * Helper functions for performing api queries on arrays of data.
26 *
27 * @package Civi\Api4\Generic
28 */
29 trait ArrayQueryActionTrait {
30
31 /**
32 * @param array $values
33 * List of all rows to be filtered
34 * @param \Civi\Api4\Generic\Result $result
35 * Object to store result
36 */
37 protected function queryArray($values, $result) {
38 $values = $this->filterArray($values);
39 $values = $this->sortArray($values);
40 // Set total count before applying limit
41 $result->rowCount = count($values);
42 $values = $this->limitArray($values);
43 $values = $this->selectArray($values);
44 $result->exchangeArray($values);
45 }
46
47 /**
48 * @param array $values
49 * @return array
50 */
51 protected function filterArray($values) {
52 if ($this->getWhere()) {
53 $values = array_filter($values, [$this, 'evaluateFilters']);
54 }
55 return array_values($values);
56 }
57
58 /**
59 * @param array $row
60 * @return bool
61 */
62 private function evaluateFilters($row) {
63 $where = array_values($this->getWhere());
64 $allConditions = in_array($where[0], ['AND', 'OR', 'NOT']) ? $where : ['AND', $where];
65 return $this->walkFilters($row, $allConditions);
66 }
67
68 /**
69 * @param array $row
70 * @param array $filters
71 * @return bool
72 * @throws \Civi\API\Exception\NotImplementedException
73 */
74 private function walkFilters($row, $filters) {
75 switch ($filters[0]) {
76 case 'AND':
77 case 'NOT':
78 $result = TRUE;
79 foreach ($filters[1] as $filter) {
80 if (!$this->walkFilters($row, $filter)) {
81 $result = FALSE;
82 break;
83 }
84 }
85 return $result == ($filters[0] == 'AND');
86
87 case 'OR':
88 $result = !count($filters[1]);
89 foreach ($filters[1] as $filter) {
90 if ($this->walkFilters($row, $filter)) {
91 return TRUE;
92 }
93 }
94 return $result;
95
96 default:
97 return $this->filterCompare($row, $filters);
98 }
99 }
100
101 /**
102 * @param array $row
103 * @param array $condition
104 * @return bool
105 * @throws \Civi\API\Exception\NotImplementedException
106 */
107 private function filterCompare($row, $condition) {
108 if (!is_array($condition)) {
109 throw new NotImplementedException('Unexpected where syntax; expecting array.');
110 }
111 $value = $row[$condition[0]] ?? NULL;
112 $operator = $condition[1];
113 $expected = $condition[2] ?? NULL;
114 switch ($operator) {
115 case '=':
116 case '!=':
117 case '<>':
118 $equal = $value == $expected;
119 // PHP is too imprecise about comparing the number 0
120 if ($expected === 0 || $expected === '0') {
121 $equal = ($value === 0 || $value === '0');
122 }
123 // PHP is too imprecise about comparing empty strings
124 if ($expected === '') {
125 $equal = ($value === '');
126 }
127 return $equal == ($operator == '=');
128
129 case 'IS NULL':
130 case 'IS NOT NULL':
131 return is_null($value) == ($operator == 'IS NULL');
132
133 case 'IS EMPTY':
134 case 'IS NOT EMPTY':
135 return empty($value) == ($operator == 'IS EMPTY');
136
137 case '>':
138 return $value > $expected;
139
140 case '>=':
141 return $value >= $expected;
142
143 case '<':
144 return $value < $expected;
145
146 case '<=':
147 return $value <= $expected;
148
149 case 'BETWEEN':
150 case 'NOT BETWEEN':
151 $between = ($value >= $expected[0] && $value <= $expected[1]);
152 return $between == ($operator == 'BETWEEN');
153
154 case 'LIKE':
155 case 'NOT LIKE':
156 $pattern = '/^' . str_replace('%', '.*', preg_quote($expected, '/')) . '$/i';
157 return !preg_match($pattern, $value) == ($operator != 'LIKE');
158
159 case 'IN':
160 return in_array($value, $expected);
161
162 case 'NOT IN':
163 return !in_array($value, $expected);
164
165 case 'CONTAINS':
166 if (is_array($value)) {
167 return in_array($expected, $value);
168 }
169 elseif (is_string($value) || is_numeric($value)) {
170 return strpos((string) $value, (string) $expected) !== FALSE;
171 }
172 return $value == $expected;
173
174 default:
175 throw new NotImplementedException("Unsupported operator: '$operator' cannot be used with array data");
176 }
177 }
178
179 /**
180 * @param $values
181 * @return array
182 */
183 protected function sortArray($values) {
184 if ($this->getOrderBy()) {
185 usort($values, [$this, 'sortCompare']);
186 }
187 return $values;
188 }
189
190 private function sortCompare($a, $b) {
191 foreach ($this->getOrderBy() as $field => $dir) {
192 $modifier = $dir == 'ASC' ? 1 : -1;
193 if (isset($a[$field]) && isset($b[$field])) {
194 if ($a[$field] == $b[$field]) {
195 continue;
196 }
197 return (strnatcasecmp($a[$field], $b[$field]) * $modifier);
198 }
199 elseif (isset($a[$field]) || isset($b[$field])) {
200 return ((isset($a[$field]) ? 1 : -1) * $modifier);
201 }
202 }
203 return 0;
204 }
205
206 /**
207 * @param $values
208 * @return array
209 */
210 protected function selectArray($values) {
211 if ($this->getSelect() === ['row_count']) {
212 $values = [['row_count' => count($values)]];
213 }
214 elseif ($this->getSelect()) {
215 // Return only fields specified by SELECT
216 foreach ($values as &$value) {
217 $value = array_intersect_key($value, array_flip($this->getSelect()));
218 }
219 }
220 else {
221 // With no SELECT specified, return all values that are keyed by plain field name; omit those with :pseudoconstant suffixes
222 foreach ($values as &$value) {
223 $value = array_filter($value, function($key) {
224 return strpos($key, ':') === FALSE;
225 }, ARRAY_FILTER_USE_KEY);
226 }
227 }
228 return $values;
229 }
230
231 /**
232 * @param $values
233 * @return array
234 */
235 protected function limitArray($values) {
236 if ($this->getOffset() || $this->getLimit()) {
237 $values = array_slice($values, $this->getOffset() ?: 0, $this->getLimit() ?: NULL);
238 }
239 return $values;
240 }
241
242 }