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