Merge pull request #21578 from civicrm/5.42
[civicrm-core.git] / Civi / Api4 / Generic / AbstractGetAction.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 namespace Civi\Api4\Generic;
14
15 use Civi\Api4\Utils\SelectUtil;
16
17 /**
18 * Base class for all `Get` api actions.
19 *
20 * @package Civi\Api4\Generic
21 *
22 * @method $this setSelect(array $selects) Set array of fields to be selected (wildcard * allowed)
23 * @method array getSelect()
24 */
25 abstract class AbstractGetAction extends AbstractQueryAction {
26
27 /**
28 * Fields to return for each $ENTITY. Defaults to all fields `[*]`.
29 *
30 * Use the * wildcard by itself to select all available fields, or use it to match similarly-named fields.
31 * E.g. `is_*` will match fields named is_primary, is_active, etc.
32 *
33 * Set to `["row_count"]` to return only the number of $ENTITIES found.
34 *
35 * @var array
36 */
37 protected $select = [];
38
39 /**
40 * Only return the number of found items.
41 *
42 * @return $this
43 */
44 public function selectRowCount() {
45 $this->select = ['row_count'];
46 return $this;
47 }
48
49 /**
50 * Adds field defaults to the where clause.
51 *
52 * Note: it will skip adding field defaults when fetching records by id,
53 * or if that field has already been added to the where clause.
54 *
55 * @throws \API_Exception
56 */
57 protected function setDefaultWhereClause() {
58 if (!$this->_itemsToGet('id')) {
59 $fields = $this->entityFields();
60 foreach ($fields as $field) {
61 if (isset($field['default_value']) && !$this->_whereContains($field['name'])) {
62 $this->addWhere($field['name'], '=', $field['default_value']);
63 }
64 }
65 }
66 }
67
68 /**
69 * Adds all standard fields matched by the * wildcard
70 *
71 * Note: this function only deals with simple wildcard expressions.
72 * It ignores those containing special characters like dots or parentheses,
73 * they are handled separately in Api4SelectQuery.
74 *
75 * @throws \API_Exception
76 */
77 protected function expandSelectClauseWildcards() {
78 if (!$this->select) {
79 $this->select = ['*'];
80 }
81 // Get expressions containing wildcards but no dots or parentheses
82 $wildFields = array_filter($this->select, function($item) {
83 return strpos($item, '*') !== FALSE && strpos($item, '.') === FALSE && strpos($item, '(') === FALSE && strpos($item, ' ') === FALSE;
84 });
85 if ($wildFields) {
86 // Wildcards should not match "Extra" fields
87 $standardFields = array_filter(array_map(function($field) {
88 return $field['type'] === 'Extra' ? NULL : $field['name'];
89 }, $this->entityFields()));
90 foreach ($wildFields as $item) {
91 $pos = array_search($item, array_values($this->select));
92 $matches = SelectUtil::getMatchingFields($item, $standardFields);
93 array_splice($this->select, $pos, 1, $matches);
94 }
95 }
96 $this->select = array_unique($this->select);
97 }
98
99 /**
100 * Helper to parse the WHERE param for getRecords to perform simple pre-filtering.
101 *
102 * This is intended to optimize some common use-cases e.g. calling the api to get
103 * one or more records by name or id.
104 *
105 * Ex: If getRecords fetches a long list of items each with a unique name,
106 * but the user has specified a single record to retrieve, you can optimize the call
107 * by checking `$this->_itemsToGet('name')` and only fetching the item(s) with that name.
108 *
109 * @param string $field
110 * @return array|null
111 */
112 protected function _itemsToGet($field) {
113 foreach ($this->where as $clause) {
114 // Look for exact-match operators (=, IN, or LIKE with no wildcard)
115 if ($clause[0] == $field && (in_array($clause[1], ['=', 'IN'], TRUE) || ($clause[1] == 'LIKE' && !(is_string($clause[2]) && strpos($clause[2], '%') !== FALSE)))) {
116 return (array) $clause[2];
117 }
118 }
119 return NULL;
120 }
121
122 /**
123 * Helper to see if field(s) should be selected by the getRecords function.
124 *
125 * Checks the SELECT, WHERE and ORDER BY params to see what fields are needed.
126 *
127 * Note that if no SELECT clause has been set then all fields should be selected
128 * and this function will return TRUE for field expressions that don't contain a :pseudoconstant suffix.
129 *
130 * @param string ...$fieldNames
131 * One or more field names to check (uses OR if multiple)
132 * @return bool
133 * Returns true if any given fields are in use.
134 */
135 protected function _isFieldSelected(string ...$fieldNames) {
136 if ((!$this->select && strpos($fieldNames[0], ':') === FALSE) || array_intersect($fieldNames, array_merge($this->select, array_keys($this->orderBy)))) {
137 return TRUE;
138 }
139 return $this->_whereContains($fieldNames);
140 }
141
142 /**
143 * Walk through the where clause and check if field(s) are in use.
144 *
145 * @param string|array $fieldName
146 * A single fieldName or an array of names (uses OR if multiple)
147 * @param array $clauses
148 * @return bool
149 * Returns true if any given fields are found in the where clause.
150 */
151 protected function _whereContains($fieldName, $clauses = NULL) {
152 if ($clauses === NULL) {
153 $clauses = $this->where;
154 }
155 $fieldName = (array) $fieldName;
156 foreach ($clauses as $clause) {
157 if (is_array($clause) && is_string($clause[0])) {
158 if (in_array($clause[0], $fieldName)) {
159 return TRUE;
160 }
161 elseif (is_array($clause[1])) {
162 return $this->_whereContains($fieldName, $clause[1]);
163 }
164 }
165 }
166 return FALSE;
167 }
168
169 /**
170 * Add one or more fields to be selected (wildcard * allowed)
171 * @param string ...$fieldNames
172 * @return $this
173 */
174 public function addSelect(string ...$fieldNames) {
175 $this->select = array_merge($this->select, $fieldNames);
176 return $this;
177 }
178
179 }