Merge pull request #23470 from eileenmcnaughton/import_dataform
[civicrm-core.git] / Civi / Api4 / Generic / Result.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 namespace Civi\Api4\Generic;
13
14 /**
15 * Container for api results.
16 *
17 * The Result object has three functions:
18 *
19 * 1. Store the results of the API call (accessible via ArrayAccess).
20 * 2. Store metadata like the Entity & Action names.
21 * - Note: some actions extend the Result object to store extra metadata.
22 * For example, BasicReplaceAction returns ReplaceResult which includes the additional $deleted property to list any items deleted by the operation.
23 * 3. Provide convenience methods like `$result->first()` and `$result->indexBy($field)`.
24 */
25 class Result extends \ArrayObject implements \JsonSerializable {
26 /**
27 * @var string
28 */
29 public $entity;
30 /**
31 * @var string
32 */
33 public $action;
34 /**
35 * @var array
36 */
37 public $debug;
38 /**
39 * Api version
40 * @var int
41 */
42 public $version = 4;
43 /**
44 * Not for public use. Instead, please use countFetched(), countMatched() and count().
45 *
46 * @var int
47 */
48 public $rowCount;
49
50 /**
51 * How many entities matched the query, regardless of LIMIT clauses.
52 *
53 * This requires that row_count is included in the SELECT.
54 *
55 * @var int
56 */
57 protected $matchedCount;
58
59 private $indexedBy;
60
61 /**
62 * Return first result.
63 * @return array|null
64 */
65 public function first() {
66 foreach ($this as $values) {
67 return $values;
68 }
69 return NULL;
70 }
71
72 /**
73 * Return last result.
74 * @return array|null
75 */
76 public function last() {
77 $items = $this->getArrayCopy();
78 return array_pop($items);
79 }
80
81 /**
82 * Return the one-and-only result record.
83 *
84 * If there are too many or too few results, then throw an exception.
85 *
86 * @return array
87 * @throws \API_Exception
88 */
89 public function single() {
90 $result = NULL;
91 foreach ($this as $values) {
92 if ($result === NULL) {
93 $result = $values;
94 }
95 else {
96 throw new \API_Exception("Expected to find one {$this->entity} record, but there were multiple.");
97 }
98 }
99
100 if ($result === NULL) {
101 throw new \API_Exception("Expected to find one {$this->entity} record, but there were zero.");
102 }
103
104 return $result;
105 }
106
107 /**
108 * @param int $index
109 * @return array|null
110 */
111 public function itemAt($index) {
112 $length = $index < 0 ? 0 - $index : $index + 1;
113 if ($length > count($this)) {
114 return NULL;
115 }
116 return array_slice(array_values($this->getArrayCopy()), $index, 1)[0];
117 }
118
119 /**
120 * Re-index the results array (which by default is non-associative)
121 *
122 * Drops any item from the results that does not contain the specified key
123 *
124 * @param string $key
125 * @return $this
126 * @throws \API_Exception
127 */
128 public function indexBy($key) {
129 $this->indexedBy = $key;
130 if (count($this)) {
131 $newResults = [];
132 foreach ($this as $values) {
133 if (isset($values[$key])) {
134 $newResults[$values[$key]] = $values;
135 }
136 }
137 if (!$newResults) {
138 throw new \API_Exception("Key $key not found in api results");
139 }
140 $this->exchangeArray($newResults);
141 }
142 return $this;
143 }
144
145 /**
146 * Returns the number of results.
147 *
148 * If row_count was included in the select fields, then this will be the
149 * number of matched entities, even if this differs from the number of
150 * entities fetched.
151 *
152 * If row_count was not included, then this returns the number of entities
153 * fetched, which may or may not be the number of matches.
154 *
155 * Your code might be easier to reason about if you use countFetched() or
156 * countMatched() instead.
157 *
158 * @return int
159 */
160 public function count() {
161 return $this->rowCount ?? parent::count();
162 }
163
164 /**
165 * Returns the number of results fetched.
166 *
167 * If a limit was used, this will be a number up to that limit.
168 *
169 * In the case that *only* the row_count was fetched, this will be zero, since no *entities* were fetched.
170 *
171 * @return int
172 */
173 public function countFetched() :int {
174 return parent::count();
175 }
176
177 /**
178 * Returns the number of results
179 *
180 * @return int
181 */
182 public function countMatched() :int {
183 if (!isset($this->matchedCount)) {
184 throw new \API_Exception("countMatched can only be used if there was no limit set or if row_count was included in the select fields.");
185 }
186 return $this->matchedCount;
187 }
188
189 /**
190 * Provides a way for API implementations to set the *matched* count.
191 *
192 * The matched count is the number of matching entities, regardless of any imposed limit clause.
193 */
194 public function setCountMatched(int $c) {
195 $this->matchedCount = $c;
196
197 // Set rowCount for backward compatibility.
198 $this->rowCount = $c;
199 }
200
201 /**
202 * Reduce each result to one field
203 *
204 * @param $name
205 * @return array
206 */
207 public function column($name) {
208 return array_column($this->getArrayCopy(), $name, $this->indexedBy);
209 }
210
211 /**
212 * @return array
213 */
214 public function jsonSerialize() {
215 return $this->getArrayCopy();
216 }
217
218 }