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