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