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\TokenRow
;
15 use Civi\ActionSchedule\Event\MailingQueryEvent
;
16 use Civi\Token\TokenProcessor
;
19 * Class CRM_Core_EntityTokens
21 * Parent class for generic entity token functionality.
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.
28 class CRM_Core_EntityTokens
extends AbstractTokenSubscriber
{
33 protected $prefetch = [];
37 * @throws \CRM_Core_Exception
39 public function evaluateToken(TokenRow
$row, $entity, $field, $prefetch = NULL) {
40 $this->prefetch
= (array) $prefetch;
41 $fieldValue = $this->getFieldValue($row, $field);
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])));
47 if ($this->isMoneyField($field)) {
48 return $row->format('text/plain')->tokens($entity, $field,
49 \CRM_Utils_Money
::format($fieldValue, $this->getCurrency($row)));
51 if ($this->isDateField($field)) {
52 return $row->format('text/plain')->tokens($entity, $field, \CRM_Utils_Date
::customFormat($fieldValue));
54 if ($this->isCustomField($field)) {
55 $row->customToken($entity, \CRM_Core_BAO_CustomField
::getKeyID($field), $this->getFieldValue($row, 'id'));
58 $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue);
63 * Metadata about the entity fields.
67 protected $fieldMetadata = [];
70 * Get the entity name for api v4 calls.
74 protected function getApiEntityName(): string {
79 * Get the entity alias to use within queries.
81 * The default has a double underscore which should prevent any
82 * ambiguity with an existing table name.
86 protected function getEntityAlias(): string {
87 return $this->getApiEntityName() . '__';
91 * Get the name of the table this token class can extend.
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.
97 public function getExtendableTableName(): string {
98 return CRM_Core_DAO_AllCoreTables
::getTableForEntityName($this->getApiEntityName());
102 * Get the relevant bao name.
104 public function getBAOName(): string {
105 return CRM_Core_DAO_AllCoreTables
::getFullName($this->getApiEntityName());
109 * Get an array of fields to be requested.
113 public function getReturnFields(): array {
114 return array_keys($this->getBasicTokens());
118 * Get all the tokens supported by this processor.
120 * @return array|string[]
122 public function getAllTokens(): array {
123 return array_merge($this->getBasicTokens(), $this->getPseudoTokens(), CRM_Utils_Token
::getCustomFieldTokens('Contribution'));
127 * Is the given field a date field.
129 * @param string $fieldName
133 public function isDateField(string $fieldName): bool {
134 return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Timestamp';
138 * Is the given field a pseudo field.
140 * @param string $fieldName
144 public function isPseudoField(string $fieldName): bool {
145 return strpos($fieldName, ':') !== FALSE;
149 * Is the given field a custom field.
151 * @param string $fieldName
155 public function isCustomField(string $fieldName) : bool {
156 return (bool) \CRM_Core_BAO_CustomField
::getKeyID($fieldName);
160 * Is the given field a date field.
162 * @param string $fieldName
166 public function isMoneyField(string $fieldName): bool {
167 return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Money';
171 * Get the metadata for the available fields.
175 protected function getFieldMetadata(): array {
176 if (empty($this->fieldMetadata
)) {
178 // Tests fail without checkPermissions = FALSE
179 $this->fieldMetadata
= (array) civicrm_api4($this->getApiEntityName(), 'getfields', ['checkPermissions' => FALSE], 'name');
181 catch (API_Exception
$e) {
182 $this->fieldMetadata
= [];
185 return $this->fieldMetadata
;
189 * Get pseudoTokens - it tokens that reflect the name or label of a pseudoconstant.
191 * @internal - this function will likely be made protected soon.
195 public function getPseudoTokens(): array {
197 foreach (array_keys($this->getBasicTokens()) as $fieldName) {
198 if ($this->isAddPseudoTokens($fieldName)) {
199 $return[$fieldName . ':label'] = $this->fieldMetadata
[$fieldName]['input_attrs']['label'];
200 $return[$fieldName . ':name'] = ts('Machine name') . ': ' . $this->fieldMetadata
[$fieldName]['input_attrs']['label'];
207 * Is this a field we should add pseudo-tokens to?
209 * Pseudo-tokens allow access to name and label fields - e.g
211 * {contribution.contribution_status_id:name} might resolve to 'Completed'
213 * @param string $fieldName
215 public function isAddPseudoTokens($fieldName): bool {
216 if ($fieldName === 'currency') {
217 // 'currency' is manually added to the skip list as an anomaly.
218 // name & label aren't that suitable for 'currency' (symbol, which
219 // possibly maps to 'abbr' would be) and we can't gather that
220 // from the metadata as yet.
223 if ($this->getFieldMetadata()[$fieldName]['type'] === 'Custom') {
224 // If we remove this early return then we get that extra nuanced goodness
225 // and support for the more portable v4 style field names
226 // on custom fields - where labels or names can be returned.
227 // At present the gap is that the metadata for the label is not accessed
228 // and tests failed on the enotice and we don't have a clear plan about
229 // v4 style custom tokens - but medium term this IF will probably go.
232 return (bool) $this->getFieldMetadata()[$fieldName]['options'];
236 * Get the value for the relevant pseudo field.
238 * @param string $realField e.g contribution_status_id
239 * @param string $pseudoKey e.g name
240 * @param int|string $fieldValue e.g 1
243 * Eg. 'Completed' in the example above.
245 * @internal function will likely be protected soon.
247 public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string {
248 if ($pseudoKey === 'name') {
249 $fieldValue = (string) CRM_Core_PseudoConstant
::getName($this->getBAOName(), $realField, $fieldValue);
251 if ($pseudoKey === 'label') {
252 $fieldValue = (string) CRM_Core_PseudoConstant
::getLabel($this->getBAOName(), $realField, $fieldValue);
254 return (string) $fieldValue;
258 * @param \Civi\Token\TokenRow $row
259 * @param string $field
262 protected function getFieldValue(TokenRow
$row, string $field) {
263 $actionSearchResult = $row->context
['actionSearchResult'];
264 $aliasedField = $this->getEntityAlias() . $field;
265 if (isset($actionSearchResult->{$aliasedField})) {
266 return $actionSearchResult->{$aliasedField};
268 $entityID = $row->context
[$this->getEntityIDField()];
269 return $this->prefetch
[$entityID][$field] ??
'';
275 public function __construct() {
276 $tokens = $this->getAllTokens();
277 parent
::__construct($this->getEntityName(), $tokens);
281 * Check if the token processor is active.
283 * @param \Civi\Token\TokenProcessor $processor
287 public function checkActive(TokenProcessor
$processor) {
288 return (!empty($processor->context
['actionMapping'])
289 // This makes the 'schema context compulsory - which feels accidental
290 // since recent discu
291 && $processor->context
['actionMapping']->getEntity()) ||
in_array($this->getEntityIDField(), $processor->context
['schema']);
295 * Alter action schedule query.
297 * @param \Civi\ActionSchedule\Event\MailingQueryEvent $e
299 public function alterActionScheduleQuery(MailingQueryEvent
$e): void
{
300 if ($e->mapping
->getEntity() !== $this->getExtendableTableName()) {
303 foreach ($this->getReturnFields() as $token) {
304 $e->query
->select('e.' . $token . ' AS ' . $this->getEntityAlias() . $token);
309 * Get tokens supporting the syntax we are migrating to.
311 * In general these are tokens that were not previously supported
312 * so we can add them in the preferred way or that we have
313 * undertaken some, as yet to be written, db update.
315 * See https://lab.civicrm.org/dev/core/-/issues/2650
318 * @throws \API_Exception
320 public function getBasicTokens(): array {
322 foreach ($this->getExposedFields() as $fieldName) {
323 $return[$fieldName] = $this->getFieldMetadata()[$fieldName]['title'];
329 * Get entity fields that should be exposed as tokens.
334 public function getExposedFields(): array {
336 foreach ($this->getFieldMetadata() as $field) {
337 if (!in_array($field['name'], $this->getSkippedFields(), TRUE)) {
338 $return[] = $field['name'];
345 * Get entity fields that should not be exposed as tokens.
349 public function getSkippedFields(): array {
350 $fields = ['contact_id'];
351 if (!CRM_Campaign_BAO_Campaign
::isCampaignEnable()) {
352 $fields[] = 'campaign_id';
360 protected function getEntityName(): string {
361 return CRM_Core_DAO_AllCoreTables
::convertEntityNameToLower($this->getApiEntityName());
364 public function getEntityIDField() {
365 return $this->getEntityName() . 'Id';
368 public function prefetch(\Civi\Token\Event\TokenValueEvent
$e): ?
array {
369 $entityIDs = $e->getTokenProcessor()->getContextValues($this->getEntityIDField());
370 if (empty($entityIDs)) {
373 $select = $this->getPrefetchFields($e);
374 $result = (array) civicrm_api4($this->getApiEntityName(), 'get', [
375 'checkPermissions' => FALSE,
376 // Note custom fields are not yet added - I need to
377 // re-do the unit tests to support custom fields first.
379 'where' => [['id', 'IN', $entityIDs]],
384 public function getCurrencyFieldName() {
389 * Get the currency to use for formatting money.
394 public function getCurrency($row): string {
395 if (!empty($this->getCurrencyFieldName())) {
396 return $this->getFieldValue($row, $this->getCurrencyFieldName()[0]);
398 return CRM_Core_Config
::singleton()->defaultCurrency
;
401 public function getPrefetchFields(\Civi\Token\Event\TokenValueEvent
$e): array {
402 return array_intersect($this->getActiveTokens($e), $this->getCurrencyFieldName(), array_keys($this->getAllTokens()));