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