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