4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 use Civi\Token\AbstractTokenSubscriber
;
14 use Civi\Token\Event\TokenRegisterEvent
;
15 use Civi\Token\Event\TokenValueEvent
;
16 use Civi\Token\TokenRow
;
17 use Civi\ActionSchedule\Event\MailingQueryEvent
;
18 use Civi\Token\TokenProcessor
;
21 * Class CRM_Core_EntityTokens
23 * Parent class for generic entity token functionality.
25 * WARNING - this class is highly likely to be temporary and
26 * to be consolidated with the TokenTrait and / or the
27 * AbstractTokenSubscriber in future. It is being used to clarify
28 * functionality but should NOT be used from outside of core tested code.
30 class CRM_Core_EntityTokens
extends AbstractTokenSubscriber
{
35 protected $prefetch = [];
38 * Register the declared tokens.
40 * @param \Civi\Token\Event\TokenRegisterEvent $e
41 * The registration event. Add new tokens using register().
43 public function registerTokens(TokenRegisterEvent
$e) {
44 if (!$this->checkActive($e->getTokenProcessor())) {
47 foreach ($this->getAllTokens() as $name => $label) {
48 if (!in_array($name, $this->getHiddenTokens(), TRUE)) {
50 'entity' => $this->entity
,
60 * @throws \CRM_Core_Exception
62 public function evaluateToken(TokenRow
$row, $entity, $field, $prefetch = NULL) {
63 $this->prefetch
= (array) $prefetch;
64 $fieldValue = $this->getFieldValue($row, $field);
65 if (is_array($fieldValue)) {
66 // eg. role_id for participant would be an array here.
67 $fieldValue = implode(',', $fieldValue);
70 if ($this->isPseudoField($field)) {
71 if (!empty($fieldValue)) {
72 // If it's set here it has already been loaded in pre-fetch.
73 return $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue);
75 // Once prefetch is fully standardised we can remove this - as long
76 // as tests pass we should be fine as tests cover this.
77 $split = explode(':', $field);
78 return $row->tokens($entity, $field, $this->getPseudoValue($split[0], $split[1], $this->getFieldValue($row, $split[0])));
80 if ($this->isCustomField($field)) {
81 $prefetchedValue = $this->getCustomFieldValue($this->getFieldValue($row, 'id'), $field);
82 if ($prefetchedValue) {
83 return $row->format('text/html')->tokens($entity, $field, $prefetchedValue);
85 return $row->customToken($entity, \CRM_Core_BAO_CustomField
::getKeyID($field), $this->getFieldValue($row, 'id'));
87 if ($this->isMoneyField($field)) {
88 return $row->format('text/plain')->tokens($entity, $field,
89 \CRM_Utils_Money
::format($fieldValue, $this->getCurrency($row)));
91 if ($this->isDateField($field)) {
93 return $row->format('text/plain')
94 ->tokens($entity, $field, new DateTime($fieldValue));
96 catch (Exception
$e) {
97 Civi
::log()->info('invalid date token');
100 $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue);
104 * Metadata about the entity fields.
108 protected $fieldMetadata = [];
111 * Get the entity name for api v4 calls.
115 protected function getApiEntityName(): string {
120 * Get the entity alias to use within queries.
122 * The default has a double underscore which should prevent any
123 * ambiguity with an existing table name.
127 protected function getEntityAlias(): string {
128 return $this->getApiEntityName() . '__';
132 * Get the name of the table this token class can extend.
134 * The default is based on the entity but some token classes,
135 * specifically the event class, latch on to other tables - ie
136 * the participant table.
138 public function getExtendableTableName(): string {
139 return CRM_Core_DAO_AllCoreTables
::getTableForEntityName($this->getApiEntityName());
143 * Get the relevant bao name.
145 public function getBAOName(): string {
146 return CRM_Core_DAO_AllCoreTables
::getFullName($this->getApiEntityName());
150 * Get an array of fields to be requested.
154 public function getReturnFields(): array {
155 return array_keys($this->getBasicTokens());
159 * Get all the tokens supported by this processor.
161 * @return array|string[]
162 * @throws \API_Exception
164 protected function getAllTokens(): array {
165 $basicTokens = $this->getBasicTokens();
166 foreach (array_keys($basicTokens) as $fieldName) {
167 // The goal is to be able to render more complete tokens
168 // (eg. actual booleans, field names, raw ids) for a more
169 // advanced audiences - ie those using conditionals
170 // and to specify that audience in the api that retrieves.
171 // But, for now, let's not advertise, given that most of these fields
172 // aren't really needed even once...
173 if ($this->isBooleanField($fieldName)) {
174 unset($basicTokens[$fieldName]);
177 return array_merge($basicTokens, $this->getPseudoTokens(), $this->getBespokeTokens(), CRM_Utils_Token
::getCustomFieldTokens($this->getApiEntityName()));
181 * Is the given field a boolean field.
183 * @param string $fieldName
187 public function isBooleanField(string $fieldName): bool {
188 return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Boolean';
192 * Is the given field a date field.
194 * @param string $fieldName
198 public function isDateField(string $fieldName): bool {
199 return in_array($this->getFieldMetadata()[$fieldName]['data_type'], ['Timestamp', 'Date'], TRUE);
203 * Is the given field a pseudo field.
205 * @param string $fieldName
209 public function isPseudoField(string $fieldName): bool {
210 return strpos($fieldName, ':') !== FALSE;
214 * Is the given field a custom field.
216 * @param string $fieldName
220 public function isCustomField(string $fieldName) : bool {
221 return (bool) \CRM_Core_BAO_CustomField
::getKeyID($fieldName);
225 * Is the given field a date field.
227 * @param string $fieldName
231 public function isMoneyField(string $fieldName): bool {
232 return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Money';
236 * Get the metadata for the available fields.
240 protected function getFieldMetadata(): array {
241 if (empty($this->fieldMetadata
)) {
243 // Tests fail without checkPermissions = FALSE
244 $this->fieldMetadata
= (array) civicrm_api4($this->getApiEntityName(), 'getfields', ['checkPermissions' => FALSE], 'name');
246 catch (API_Exception
$e) {
247 $this->fieldMetadata
= [];
250 return $this->fieldMetadata
;
254 * Get pseudoTokens - it tokens that reflect the name or label of a pseudoconstant.
256 * @internal - this function will likely be made protected soon.
260 public function getPseudoTokens(): array {
262 foreach (array_keys($this->getBasicTokens()) as $fieldName) {
263 if ($this->isAddPseudoTokens($fieldName)) {
264 $fieldLabel = $this->fieldMetadata
[$fieldName]['input_attrs']['label'] ??
$this->fieldMetadata
[$fieldName]['label'];
265 $return[$fieldName . ':label'] = $fieldLabel;
266 $return[$fieldName . ':name'] = ts('Machine name') . ': ' . $fieldLabel;
268 if ($this->isBooleanField($fieldName)) {
269 $return[$fieldName . ':label'] = $this->getFieldMetadata()[$fieldName]['title'];
276 * Get any tokens with custom calculation.
278 public function getBespokeTokens(): array {
283 * Is this a field we should add pseudo-tokens to?
285 * Pseudo-tokens allow access to name and label fields - e.g
287 * {contribution.contribution_status_id:name} might resolve to 'Completed'
289 * @param string $fieldName
291 public function isAddPseudoTokens($fieldName): bool {
292 if (in_array($fieldName, $this->getCurrencyFieldName())) {
293 // 'currency' is manually added to the skip list as an anomaly.
294 // name & label aren't that suitable for 'currency' (symbol, which
295 // possibly maps to 'abbr' would be) and we can't gather that
296 // from the metadata as yet.
299 if ($this->getFieldMetadata()[$fieldName]['type'] === 'Custom') {
300 // If we remove this early return then we get that extra nuanced goodness
301 // and support for the more portable v4 style field names
302 // on custom fields - where labels or names can be returned.
303 // At present the gap is that the metadata for the label is not accessed
304 // and tests failed on the enotice and we don't have a clear plan about
305 // v4 style custom tokens - but medium term this IF will probably go.
308 return (bool) ($this->getFieldMetadata()[$fieldName]['options'] ||
!empty($this->getFieldMetadata()[$fieldName]['suffixes']));
312 * Get the value for the relevant pseudo field.
314 * @param string $realField e.g contribution_status_id
315 * @param string $pseudoKey e.g name
316 * @param int|string $fieldValue e.g 1
319 * Eg. 'Completed' in the example above.
321 * @internal function will likely be protected soon.
323 public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string {
324 if ($pseudoKey === 'name') {
325 $fieldValue = (string) CRM_Core_PseudoConstant
::getName($this->getBAOName(), $realField, $fieldValue);
327 if ($pseudoKey === 'label') {
328 $fieldValue = (string) CRM_Core_PseudoConstant
::getLabel($this->getBAOName(), $realField, $fieldValue);
330 return (string) $fieldValue;
334 * @param \Civi\Token\TokenRow $row
335 * @param string $field
338 protected function getFieldValue(TokenRow
$row, string $field) {
339 $entityName = $this->getEntityName();
340 if (isset($row->context
[$entityName][$field])) {
341 return $row->context
[$entityName][$field];
344 $actionSearchResult = $row->context
['actionSearchResult'];
345 $aliasedField = $this->getEntityAlias() . $field;
346 if (isset($actionSearchResult->{$aliasedField})) {
347 return $actionSearchResult->{$aliasedField};
349 $entityID = $row->context
[$this->getEntityIDField()];
350 if ($field === 'id') {
353 return $this->prefetch
[$entityID][$field] ??
'';
359 public function __construct() {
360 $tokens = $this->getAllTokens();
361 parent
::__construct($this->getEntityName(), $tokens);
365 * Check if the token processor is active.
367 * @param \Civi\Token\TokenProcessor $processor
371 public function checkActive(TokenProcessor
$processor) {
372 return (!empty($processor->context
['actionMapping'])
373 // This makes the 'schema context compulsory - which feels accidental
374 // since recent discu
375 && $processor->context
['actionMapping']->getEntity()) ||
in_array($this->getEntityIDField(), $processor->context
['schema']);
379 * Alter action schedule query.
381 * @param \Civi\ActionSchedule\Event\MailingQueryEvent $e
383 public function alterActionScheduleQuery(MailingQueryEvent
$e): void
{
384 if ($e->mapping
->getEntity() !== $this->getExtendableTableName()) {
387 foreach ($this->getReturnFields() as $token) {
388 $e->query
->select('e.' . $token . ' AS ' . $this->getEntityAlias() . $token);
393 * Get tokens to be suppressed from the widget.
395 * Note this is expected to be an interim function. Now we are no
396 * longer working around the parent function we can just define them once...
397 * with metadata, in a future refactor.
399 protected function getHiddenTokens(): array {
404 * Get tokens supporting the syntax we are migrating to.
406 * In general these are tokens that were not previously supported
407 * so we can add them in the preferred way or that we have
408 * undertaken some, as yet to be written, db update.
410 * See https://lab.civicrm.org/dev/core/-/issues/2650
413 * @throws \API_Exception
415 public function getBasicTokens(): array {
417 foreach ($this->getExposedFields() as $fieldName) {
418 // Custom fields are still added v3 style - we want to keep v4 naming 'unpoluted'
419 // for now to allow us to consider how to handle names vs labels vs values
420 // and other raw vs not raw options.
421 if ($this->getFieldMetadata()[$fieldName]['type'] !== 'Custom') {
422 $return[$fieldName] = $this->getFieldMetadata()[$fieldName]['title'];
429 * Get entity fields that should be exposed as tokens.
434 public function getExposedFields(): array {
436 foreach ($this->getFieldMetadata() as $field) {
437 if (!in_array($field['name'], $this->getSkippedFields(), TRUE)) {
438 $return[] = $field['name'];
445 * Get entity fields that should not be exposed as tokens.
449 public function getSkippedFields(): array {
450 // tags is offered in 'case' & is one of the only fields that is
451 // 'not a real field' offered up by case - seems like an oddity
452 // we should skip at the top level for now.
453 $fields = ['contact_id', 'tags'];
454 if (!CRM_Campaign_BAO_Campaign
::isCampaignEnable()) {
455 $fields[] = 'campaign_id';
463 protected function getEntityName(): string {
464 return CRM_Core_DAO_AllCoreTables
::convertEntityNameToLower($this->getApiEntityName());
467 public function getEntityIDField(): string {
468 return $this->getEntityName() . 'Id';
471 public function prefetch(TokenValueEvent
$e): ?
array {
472 $entityIDs = $e->getTokenProcessor()->getContextValues($this->getEntityIDField());
473 if (empty($entityIDs)) {
476 $select = $this->getPrefetchFields($e);
477 $result = (array) civicrm_api4($this->getApiEntityName(), 'get', [
478 'checkPermissions' => FALSE,
479 // Note custom fields are not yet added - I need to
480 // re-do the unit tests to support custom fields first.
482 'where' => [['id', 'IN', $entityIDs]],
487 public function getCurrencyFieldName() {
492 * Get the currency to use for formatting money.
497 public function getCurrency($row): string {
498 if (!empty($this->getCurrencyFieldName())) {
499 return $this->getFieldValue($row, $this->getCurrencyFieldName()[0]);
501 return CRM_Core_Config
::singleton()->defaultCurrency
;
505 * Get the fields required to prefetch the entity.
507 * @param \Civi\Token\Event\TokenValueEvent $e
510 * @throws \API_Exception
512 public function getPrefetchFields(TokenValueEvent
$e): array {
513 $allTokens = array_keys($this->getAllTokens());
514 $requiredFields = array_intersect($this->getActiveTokens($e), $allTokens);
515 if (empty($requiredFields)) {
518 $requiredFields = array_merge($requiredFields, array_intersect($allTokens, array_merge(['id'], $this->getCurrencyFieldName())));
519 foreach ($this->getDependencies() as $field => $required) {
520 if (in_array($field, $this->getActiveTokens($e), TRUE)) {
521 foreach ((array) $required as $key) {
522 $requiredFields[] = $key;
526 return $requiredFields;
530 * Get fields which need to be returned to render another token.
534 public function getDependencies(): array {
539 * Get the apiv4 style custom field name.
545 protected function getCustomFieldName(int $id): string {
546 foreach ($this->getFieldMetadata() as $key => $field) {
547 if (($field['custom_field_id'] ??
NULL) === $id) {
555 * @param string $field eg. 'custom_1'
557 * @return array|string|void|null $mixed
559 * @throws \CRM_Core_Exception
561 protected function getCustomFieldValue($entityID, string $field) {
562 $id = str_replace('custom_', '', $field);
563 $value = $this->prefetch
[$entityID][$this->getCustomFieldName($id)] ??
NULL;
564 if ($value !== NULL) {
565 return CRM_Core_BAO_CustomField
::displayValue($value, $id);