Merge pull request #21287 from colemanw/afformButton
[civicrm-core.git] / CRM / Core / EntityTokens.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 use Civi\Token\AbstractTokenSubscriber;
14 use Civi\Token\TokenRow;
15 use Civi\ActionSchedule\Event\MailingQueryEvent;
16 use Civi\Token\TokenProcessor;
17
18 /**
19 * Class CRM_Core_EntityTokens
20 *
21 * Parent class for generic entity token functionality.
22 *
23 * WARNING - this class is highly likely to be temporary and
24 * to be consolidated with the TokenTrait and / or the
25 * AbstractTokenSubscriber in future. It is being used to clarify
26 * functionality but should NOT be used from outside of core tested code.
27 */
28 class CRM_Core_EntityTokens extends AbstractTokenSubscriber {
29
30 /**
31 * @var array
32 */
33 protected $prefetch = [];
34
35 /**
36 * @inheritDoc
37 * @throws \CRM_Core_Exception
38 */
39 public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) {
40 $this->prefetch = (array) $prefetch;
41 $fieldValue = $this->getFieldValue($row, $field);
42
43 if ($this->isPseudoField($field)) {
44 $split = explode(':', $field);
45 return $row->tokens($entity, $field, $this->getPseudoValue($split[0], $split[1], $this->getFieldValue($row, $split[0])));
46 }
47 if ($this->isMoneyField($field)) {
48 return $row->format('text/plain')->tokens($entity, $field,
49 \CRM_Utils_Money::format($fieldValue, $this->getCurrency($row)));
50 }
51 if ($this->isDateField($field)) {
52 return $row->format('text/plain')->tokens($entity, $field, \CRM_Utils_Date::customFormat($fieldValue));
53 }
54 if ($this->isCustomField($field)) {
55 $row->customToken($entity, \CRM_Core_BAO_CustomField::getKeyID($field), $this->getFieldValue($row, 'id'));
56 }
57 else {
58 $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue);
59 }
60 }
61
62 /**
63 * Metadata about the entity fields.
64 *
65 * @var array
66 */
67 protected $fieldMetadata = [];
68
69 /**
70 * Get the entity name for api v4 calls.
71 *
72 * @return string
73 */
74 protected function getApiEntityName(): string {
75 return '';
76 }
77
78 /**
79 * Get the entity alias to use within queries.
80 *
81 * The default has a double underscore which should prevent any
82 * ambiguity with an existing table name.
83 *
84 * @return string
85 */
86 protected function getEntityAlias(): string {
87 return $this->getApiEntityName() . '__';
88 }
89
90 /**
91 * Get the name of the table this token class can extend.
92 *
93 * The default is based on the entity but some token classes,
94 * specifically the event class, latch on to other tables - ie
95 * the participant table.
96 */
97 public function getExtendableTableName(): string {
98 return CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getApiEntityName());
99 }
100
101 /**
102 * Get the relevant bao name.
103 */
104 public function getBAOName(): string {
105 return CRM_Core_DAO_AllCoreTables::getFullName($this->getApiEntityName());
106 }
107
108 /**
109 * Get an array of fields to be requested.
110 *
111 * @return string[]
112 */
113 public function getReturnFields(): array {
114 return array_keys($this->getBasicTokens());
115 }
116
117 /**
118 * Get all the tokens supported by this processor.
119 *
120 * @return array|string[]
121 */
122 public function getAllTokens(): array {
123 return array_merge($this->getBasicTokens(), $this->getPseudoTokens(), CRM_Utils_Token::getCustomFieldTokens('Contribution'));
124 }
125
126 /**
127 * Is the given field a date field.
128 *
129 * @param string $fieldName
130 *
131 * @return bool
132 */
133 public function isDateField(string $fieldName): bool {
134 return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Timestamp';
135 }
136
137 /**
138 * Is the given field a pseudo field.
139 *
140 * @param string $fieldName
141 *
142 * @return bool
143 */
144 public function isPseudoField(string $fieldName): bool {
145 return strpos($fieldName, ':') !== FALSE;
146 }
147
148 /**
149 * Is the given field a custom field.
150 *
151 * @param string $fieldName
152 *
153 * @return bool
154 */
155 public function isCustomField(string $fieldName) : bool {
156 return (bool) \CRM_Core_BAO_CustomField::getKeyID($fieldName);
157 }
158
159 /**
160 * Is the given field a date field.
161 *
162 * @param string $fieldName
163 *
164 * @return bool
165 */
166 public function isMoneyField(string $fieldName): bool {
167 return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Money';
168 }
169
170 /**
171 * Get the metadata for the available fields.
172 *
173 * @return array
174 */
175 protected function getFieldMetadata(): array {
176 if (empty($this->fieldMetadata)) {
177 try {
178 // Tests fail without checkPermissions = FALSE
179 $this->fieldMetadata = (array) civicrm_api4($this->getApiEntityName(), 'getfields', ['checkPermissions' => FALSE], 'name');
180 }
181 catch (API_Exception $e) {
182 $this->fieldMetadata = [];
183 }
184 }
185 return $this->fieldMetadata;
186 }
187
188 /**
189 * Get pseudoTokens - it tokens that reflect the name or label of a pseudoconstant.
190 *
191 * @internal - this function will likely be made protected soon.
192 *
193 * @return array
194 */
195 public function getPseudoTokens(): array {
196 $return = [];
197 foreach (array_keys($this->getBasicTokens()) as $fieldName) {
198 if ($this->isAddPseudoTokens($fieldName)) {
199 $fieldLabel = $this->fieldMetadata[$fieldName]['input_attrs']['label'] ?? $this->fieldMetadata[$fieldName]['label'];
200 $return[$fieldName . ':label'] = $fieldLabel;
201 $return[$fieldName . ':name'] = ts('Machine name') . ': ' . $fieldLabel;
202 }
203 }
204 return $return;
205 }
206
207 /**
208 * Is this a field we should add pseudo-tokens to?
209 *
210 * Pseudo-tokens allow access to name and label fields - e.g
211 *
212 * {contribution.contribution_status_id:name} might resolve to 'Completed'
213 *
214 * @param string $fieldName
215 */
216 public function isAddPseudoTokens($fieldName): bool {
217 if ($fieldName === 'currency') {
218 // 'currency' is manually added to the skip list as an anomaly.
219 // name & label aren't that suitable for 'currency' (symbol, which
220 // possibly maps to 'abbr' would be) and we can't gather that
221 // from the metadata as yet.
222 return FALSE;
223 }
224 if ($this->getFieldMetadata()[$fieldName]['type'] === 'Custom') {
225 // If we remove this early return then we get that extra nuanced goodness
226 // and support for the more portable v4 style field names
227 // on custom fields - where labels or names can be returned.
228 // At present the gap is that the metadata for the label is not accessed
229 // and tests failed on the enotice and we don't have a clear plan about
230 // v4 style custom tokens - but medium term this IF will probably go.
231 return FALSE;
232 }
233 return (bool) ($this->getFieldMetadata()[$fieldName]['options'] || !empty($this->getFieldMetadata()[$fieldName]['suffixes']));
234 }
235
236 /**
237 * Get the value for the relevant pseudo field.
238 *
239 * @param string $realField e.g contribution_status_id
240 * @param string $pseudoKey e.g name
241 * @param int|string $fieldValue e.g 1
242 *
243 * @return string
244 * Eg. 'Completed' in the example above.
245 *
246 * @internal function will likely be protected soon.
247 */
248 public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string {
249 if ($pseudoKey === 'name') {
250 $fieldValue = (string) CRM_Core_PseudoConstant::getName($this->getBAOName(), $realField, $fieldValue);
251 }
252 if ($pseudoKey === 'label') {
253 $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($this->getBAOName(), $realField, $fieldValue);
254 }
255 return (string) $fieldValue;
256 }
257
258 /**
259 * @param \Civi\Token\TokenRow $row
260 * @param string $field
261 * @return string|int
262 */
263 protected function getFieldValue(TokenRow $row, string $field) {
264 $actionSearchResult = $row->context['actionSearchResult'];
265 $aliasedField = $this->getEntityAlias() . $field;
266 if (isset($actionSearchResult->{$aliasedField})) {
267 return $actionSearchResult->{$aliasedField};
268 }
269 $entityID = $row->context[$this->getEntityIDField()];
270 return $this->prefetch[$entityID][$field] ?? '';
271 }
272
273 /**
274 * Class constructor.
275 */
276 public function __construct() {
277 $tokens = $this->getAllTokens();
278 parent::__construct($this->getEntityName(), $tokens);
279 }
280
281 /**
282 * Check if the token processor is active.
283 *
284 * @param \Civi\Token\TokenProcessor $processor
285 *
286 * @return bool
287 */
288 public function checkActive(TokenProcessor $processor) {
289 return (!empty($processor->context['actionMapping'])
290 // This makes the 'schema context compulsory - which feels accidental
291 // since recent discu
292 && $processor->context['actionMapping']->getEntity()) || in_array($this->getEntityIDField(), $processor->context['schema']);
293 }
294
295 /**
296 * Alter action schedule query.
297 *
298 * @param \Civi\ActionSchedule\Event\MailingQueryEvent $e
299 */
300 public function alterActionScheduleQuery(MailingQueryEvent $e): void {
301 if ($e->mapping->getEntity() !== $this->getExtendableTableName()) {
302 return;
303 }
304 foreach ($this->getReturnFields() as $token) {
305 $e->query->select('e.' . $token . ' AS ' . $this->getEntityAlias() . $token);
306 }
307 }
308
309 /**
310 * Get tokens supporting the syntax we are migrating to.
311 *
312 * In general these are tokens that were not previously supported
313 * so we can add them in the preferred way or that we have
314 * undertaken some, as yet to be written, db update.
315 *
316 * See https://lab.civicrm.org/dev/core/-/issues/2650
317 *
318 * @return string[]
319 * @throws \API_Exception
320 */
321 public function getBasicTokens(): array {
322 $return = [];
323 foreach ($this->getExposedFields() as $fieldName) {
324 // Custom fields are still added v3 style - we want to keep v4 naming 'unpoluted'
325 // for now to allow us to consider how to handle names vs labels vs values
326 // and other raw vs not raw options.
327 if ($this->getFieldMetadata()[$fieldName]['type'] !== 'Custom') {
328 $return[$fieldName] = $this->getFieldMetadata()[$fieldName]['title'];
329 }
330 }
331 return $return;
332 }
333
334 /**
335 * Get entity fields that should be exposed as tokens.
336 *
337 * @return string[]
338 *
339 */
340 public function getExposedFields(): array {
341 $return = [];
342 foreach ($this->getFieldMetadata() as $field) {
343 if (!in_array($field['name'], $this->getSkippedFields(), TRUE)) {
344 $return[] = $field['name'];
345 }
346 }
347 return $return;
348 }
349
350 /**
351 * Get entity fields that should not be exposed as tokens.
352 *
353 * @return string[]
354 */
355 public function getSkippedFields(): array {
356 $fields = ['contact_id'];
357 if (!CRM_Campaign_BAO_Campaign::isCampaignEnable()) {
358 $fields[] = 'campaign_id';
359 }
360 return $fields;
361 }
362
363 /**
364 * @return string
365 */
366 protected function getEntityName(): string {
367 return CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($this->getApiEntityName());
368 }
369
370 public function getEntityIDField() {
371 return $this->getEntityName() . 'Id';
372 }
373
374 public function prefetch(\Civi\Token\Event\TokenValueEvent $e): ?array {
375 $entityIDs = $e->getTokenProcessor()->getContextValues($this->getEntityIDField());
376 if (empty($entityIDs)) {
377 return [];
378 }
379 $select = $this->getPrefetchFields($e);
380 $result = (array) civicrm_api4($this->getApiEntityName(), 'get', [
381 'checkPermissions' => FALSE,
382 // Note custom fields are not yet added - I need to
383 // re-do the unit tests to support custom fields first.
384 'select' => $select,
385 'where' => [['id', 'IN', $entityIDs]],
386 ], 'id');
387 return $result;
388 }
389
390 public function getCurrencyFieldName() {
391 return [];
392 }
393
394 /**
395 * Get the currency to use for formatting money.
396 * @param $row
397 *
398 * @return string
399 */
400 public function getCurrency($row): string {
401 if (!empty($this->getCurrencyFieldName())) {
402 return $this->getFieldValue($row, $this->getCurrencyFieldName()[0]);
403 }
404 return CRM_Core_Config::singleton()->defaultCurrency;
405 }
406
407 public function getPrefetchFields(\Civi\Token\Event\TokenValueEvent $e): array {
408 return array_intersect($this->getActiveTokens($e), $this->getCurrencyFieldName(), array_keys($this->getAllTokens()));
409 }
410
411 }