Merge pull request #22841 from braders/state-class-tidy
[civicrm-core.git] / Civi / Token / TokenRow.php
CommitLineData
8adcd073
TO
1<?php
2namespace Civi\Token;
8adcd073 3
f70a513f
EM
4use Brick\Money\Money;
5
8adcd073
TO
6/**
7 * Class TokenRow
f70a513f 8 *
8adcd073
TO
9 * @package Civi\Token
10 *
86368936
TO
11 * A TokenRow is a helper/stub providing simplified access to the TokenProcessor.
12 * There are two common cases for using the TokenRow stub:
8adcd073 13 *
86368936
TO
14 * (1) When setting up a job, you may specify general/baseline info.
15 * This is called the "context" data. Here, we create two rows:
8adcd073 16 *
0b882a86 17 * ```
86368936
TO
18 * $proc->addRow()->context('contact_id', 123);
19 * $proc->addRow()->context('contact_id', 456);
0b882a86 20 * ```
8adcd073 21 *
86368936
TO
22 * (2) When defining a token (eg `{profile.viewUrl}`), you might read the
23 * context-data (`contact_id`) and set the token-data (`profile => viewUrl`):
8adcd073 24 *
0b882a86 25 * ```
86368936
TO
26 * foreach ($proc->getRows() as $row) {
27 * $row->tokens('profile', [
28 * 'viewUrl' => 'http://example.com/profile?cid=' . urlencode($row->context['contact_id'];
29 * ]);
30 * }
0b882a86 31 * ```
86368936
TO
32 *
33 * The context and tokens can be accessed using either methods or attributes.
34 *
0b882a86 35 * ```
86368936
TO
36 * # Setting context data
37 * $row->context('contact_id', 123);
38 * $row->context(['contact_id' => 123]);
39 *
40 * # Setting token data
41 * $row->tokens('profile', ['viewUrl' => 'http://example.com/profile?cid=123']);
42 * $row->tokens('profile', 'viewUrl, 'http://example.com/profile?cid=123');
8adcd073 43 *
86368936 44 * # Reading context data
8adcd073 45 * echo $row->context['contact_id'];
86368936
TO
46 *
47 * # Reading token data
8adcd073 48 * echo $row->tokens['profile']['viewUrl'];
0b882a86 49 * ```
86368936
TO
50 *
51 * Note: The methods encourage a "fluent" style. They were written for PHP 5.3
52 * (eg before short-array syntax was supported) and are fairly flexible about
53 * input notations (e.g. `context(string $key, mixed $value)` vs `context(array $keyValuePairs)`).
8adcd073 54 *
86368936
TO
55 * Note: An instance of `TokenRow` is a stub which only contains references to the
56 * main data in `TokenProcessor`. There may be several `TokenRow` stubs
57 * referencing the same `TokenProcessor`. You can think of `TokenRow` objects as
58 * lightweight and disposable.
8adcd073
TO
59 */
60class TokenRow {
61
62 /**
86368936
TO
63 * The token-processor is where most data is actually stored.
64 *
65 * Note: Not intended for public usage. However, this is marked public to allow
66 * interaction classes in this package (`TokenProcessor`<=>`TokenRow`<=>`TokenRowContext`).
67 *
8adcd073
TO
68 * @var TokenProcessor
69 */
70 public $tokenProcessor;
71
86368936
TO
72 /**
73 * Row ID - the record within TokenProcessor that we're accessing.
74 *
75 * @var int
76 */
8adcd073
TO
77 public $tokenRow;
78
86368936
TO
79 /**
80 * The MIME type associated with new token-values.
81 *
82 * This is generally manipulated as part of a fluent chain, eg
83 *
84 * $row->format('text/plain')->token(['display_name', 'Alice Bobdaughter']);
85 *
86 * @var string
87 */
8adcd073
TO
88 public $format;
89
90 /**
09c2328a 91 * @var array|\ArrayAccess
8adcd073 92 * List of token values.
86368936
TO
93 * This is a facade for the TokenProcessor::$rowValues.
94 * Ex: ['contact' => ['display_name' => 'Alice']]
8adcd073
TO
95 */
96 public $tokens;
97
98 /**
09c2328a 99 * @var array|\ArrayAccess
8adcd073 100 * List of context values.
86368936
TO
101 * This is a facade for the TokenProcessor::$rowContexts.
102 * Ex: ['controller' => 'CRM_Foo_Bar']
8adcd073
TO
103 */
104 public $context;
105
106 public function __construct(TokenProcessor $tokenProcessor, $key) {
107 $this->tokenProcessor = $tokenProcessor;
108 $this->tokenRow = $key;
34f3bbd9
SL
109 // Set a default.
110 $this->format('text/plain');
8adcd073
TO
111 $this->context = new TokenRowContext($tokenProcessor, $key);
112 }
113
114 /**
115 * @param string $format
4b350175 116 * @return TokenRow
8adcd073
TO
117 */
118 public function format($format) {
119 $this->format = $format;
120 $this->tokens = &$this->tokenProcessor->rowValues[$this->tokenRow][$format];
121 return $this;
122 }
123
124 /**
125 * Update the value of a context element.
126 *
127 * @param string|array $a
128 * @param mixed $b
4b350175 129 * @return TokenRow
8adcd073
TO
130 */
131 public function context($a = NULL, $b = NULL) {
132 if (is_array($a)) {
133 \CRM_Utils_Array::extend($this->tokenProcessor->rowContexts[$this->tokenRow], $a);
134 }
135 elseif (is_array($b)) {
136 \CRM_Utils_Array::extend($this->tokenProcessor->rowContexts[$this->tokenRow][$a], $b);
137 }
138 else {
139 $this->tokenProcessor->rowContexts[$this->tokenRow][$a] = $b;
140 }
141 return $this;
142 }
143
144 /**
145 * Update the value of a token.
146 *
33b5e6bd
EM
147 * If you are reading this it probably means you can't follow this function.
148 * Don't worry - I've stared at it & all I see is a bunch of letters. However,
149 * the answer to your problem is almost certainly that you are passing in null
150 * rather than an empty string for 'c'.
151 *
8adcd073
TO
152 * @param string|array $a
153 * @param string|array $b
154 * @param mixed $c
4b350175 155 * @return TokenRow
8adcd073
TO
156 */
157 public function tokens($a = NULL, $b = NULL, $c = NULL) {
158 if (is_array($a)) {
159 \CRM_Utils_Array::extend($this->tokens, $a);
160 }
161 elseif (is_array($b)) {
162 \CRM_Utils_Array::extend($this->tokens[$a], $b);
163 }
164 elseif (is_array($c)) {
165 \CRM_Utils_Array::extend($this->tokens[$a][$b], $c);
166 }
167 elseif ($c === NULL) {
168 $this->tokens[$a] = $b;
169 }
170 else {
171 $this->tokens[$a][$b] = $c;
172 }
173 return $this;
174 }
175
4e9b6a62 176 /**
177 * Update the value of a custom field token.
178 *
179 * @param string $entity
8640061b 180 * @param int $customFieldID
181 * @param int $entityID
4e9b6a62 182 * @return TokenRow
183 */
8640061b 184 public function customToken($entity, $customFieldID, $entityID) {
2a7cae66
EM
185 $customFieldName = 'custom_' . $customFieldID;
186 $record = civicrm_api3($entity, 'getSingle', [
8640061b 187 'return' => $customFieldName,
34f3bbd9 188 'id' => $entityID,
e5e0c47b
AS
189 ]);
190 $fieldValue = \CRM_Utils_Array::value($customFieldName, $record, '');
8640061b 191
192 // format the raw custom field value into proper display value
b5654a0d 193 if (isset($fieldValue)) {
2a7cae66 194 $fieldValue = (string) \CRM_Core_BAO_CustomField::displayValue($fieldValue, $customFieldID);
8640061b 195 }
196
9a88bc66 197 return $this->format('text/html')->tokens($entity, $customFieldName, $fieldValue);
4e9b6a62 198 }
199
2045389a
TO
200 /**
201 * Update the value of a token. Apply formatting based on DB schema.
202 *
203 * @param string $tokenEntity
204 * @param string $tokenField
205 * @param string $baoName
9c1b96ce 206 * @param string $baoField
2045389a 207 * @param mixed $fieldValue
ee299f14
TO
208 * @return TokenRow
209 * @throws \CRM_Core_Exception
2045389a
TO
210 */
211 public function dbToken($tokenEntity, $tokenField, $baoName, $baoField, $fieldValue) {
17b6f179 212 \CRM_Core_Error::deprecatedFunctionWarning('no alternative');
2045389a
TO
213 if ($fieldValue === NULL || $fieldValue === '') {
214 return $this->tokens($tokenEntity, $tokenField, '');
215 }
216
217 $fields = $baoName::fields();
218 if (!empty($fields[$baoField]['pseudoconstant'])) {
219 $options = $baoName::buildOptions($baoField, 'get');
220 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, $options[$fieldValue]);
221 }
222
223 switch ($fields[$baoField]['type']) {
224 case \CRM_Utils_Type::T_DATE + \CRM_Utils_Type::T_TIME:
225 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, \CRM_Utils_Date::customFormat($fieldValue));
226
227 case \CRM_Utils_Type::T_MONEY:
228 // Is this something you should ever use? Seems like you need more context
229 // to know which currency to use.
230 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, \CRM_Utils_Money::format($fieldValue));
231
232 case \CRM_Utils_Type::T_STRING:
233 case \CRM_Utils_Type::T_BOOLEAN:
234 case \CRM_Utils_Type::T_INT:
235 case \CRM_Utils_Type::T_TEXT:
236 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, $fieldValue);
237
238 }
239
240 throw new \CRM_Core_Exception("Cannot format token for field '$baoField' in '$baoName'");
241 }
242
8adcd073
TO
243 /**
244 * Auto-convert between different formats
54957108 245 *
246 * @param string $format
247 *
4b350175 248 * @return TokenRow
8adcd073
TO
249 */
250 public function fill($format = NULL) {
251 if ($format === NULL) {
252 $format = $this->format;
253 }
254
255 if (!isset($this->tokenProcessor->rowValues[$this->tokenRow]['text/html'])) {
c64f69d9 256 $this->tokenProcessor->rowValues[$this->tokenRow]['text/html'] = [];
8adcd073
TO
257 }
258 if (!isset($this->tokenProcessor->rowValues[$this->tokenRow]['text/plain'])) {
c64f69d9 259 $this->tokenProcessor->rowValues[$this->tokenRow]['text/plain'] = [];
8adcd073
TO
260 }
261
262 $htmlTokens = &$this->tokenProcessor->rowValues[$this->tokenRow]['text/html'];
263 $textTokens = &$this->tokenProcessor->rowValues[$this->tokenRow]['text/plain'];
264
265 switch ($format) {
266 case 'text/html':
267 // Plain => HTML.
268 foreach ($textTokens as $entity => $values) {
c64f69d9 269 $entityFields = civicrm_api3($entity, "getFields", ['api_action' => 'get']);
8adcd073
TO
270 foreach ($values as $field => $value) {
271 if (!isset($htmlTokens[$entity][$field])) {
ee2eb45c 272 // CRM-18420 - Activity Details Field are enclosed within <p>,
273 // hence if $body_text is empty, htmlentities will lead to
274 // conversion of these tags resulting in raw HTML.
275 if ($entity == 'activity' && $field == 'details') {
276 $htmlTokens[$entity][$field] = $value;
277 }
65ddce7b
AS
278 elseif (\CRM_Utils_Array::value('data_type', \CRM_Utils_Array::value($field, $entityFields['values'])) == 'Memo') {
279 // Memo fields aka custom fields of type Note are html.
26219967 280 $htmlTokens[$entity][$field] = \CRM_Utils_String::purifyHTML($value);
65ddce7b 281 }
ee2eb45c 282 else {
e8d12345 283 $htmlTokens[$entity][$field] = is_object($value) ? $value : htmlentities($value, ENT_QUOTES);
ee2eb45c 284 }
8adcd073
TO
285 }
286 }
287 }
288 break;
289
290 case 'text/plain':
291 // HTML => Plain.
292 foreach ($htmlTokens as $entity => $values) {
293 foreach ($values as $field => $value) {
f70a513f 294 if (!$value instanceof \DateTime && !$value instanceof Money) {
defba8ff
EM
295 $value = html_entity_decode(strip_tags($value));
296 }
8adcd073 297 if (!isset($textTokens[$entity][$field])) {
defba8ff 298 $textTokens[$entity][$field] = $value;
8adcd073
TO
299 }
300 }
301 }
302 break;
303
304 default:
defba8ff 305 throw new \RuntimeException('Invalid format');
8adcd073
TO
306 }
307
308 return $this;
309 }
310
311 /**
312 * Render a message.
313 *
314 * @param string $name
315 * The name previously registered with TokenProcessor::addMessage.
316 * @return string
317 * Fully rendered message, with tokens merged.
318 */
319 public function render($name) {
320 return $this->tokenProcessor->render($name, $this);
321 }
322
323}
324
325/**
326 * Class TokenRowContext
327 * @package Civi\Token
328 *
329 * Combine the row-context and general-context into a single array-like facade.
330 */
331class TokenRowContext implements \ArrayAccess, \IteratorAggregate, \Countable {
332
333 /**
334 * @var TokenProcessor
335 */
336 protected $tokenProcessor;
337
338 protected $tokenRow;
339
340 /**
e8e8f3ad 341 * Class constructor.
342 *
343 * @param array $tokenProcessor
344 * @param array $tokenRow
8adcd073
TO
345 */
346 public function __construct($tokenProcessor, $tokenRow) {
347 $this->tokenProcessor = $tokenProcessor;
348 $this->tokenRow = $tokenRow;
349 }
350
e8e8f3ad 351 /**
352 * Does offset exist.
353 *
354 * @param mixed $offset
355 *
356 * @return bool
357 */
8adcd073 358 public function offsetExists($offset) {
34f3bbd9 359 return isset($this->tokenProcessor->rowContexts[$this->tokenRow][$offset])
8adcd073
TO
360 || isset($this->tokenProcessor->context[$offset]);
361 }
362
e8e8f3ad 363 /**
364 * Get offset.
365 *
366 * @param string $offset
367 *
368 * @return string
369 */
8adcd073
TO
370 public function &offsetGet($offset) {
371 if (isset($this->tokenProcessor->rowContexts[$this->tokenRow][$offset])) {
372 return $this->tokenProcessor->rowContexts[$this->tokenRow][$offset];
373 }
374 if (isset($this->tokenProcessor->context[$offset])) {
375 return $this->tokenProcessor->context[$offset];
376 }
377 $val = NULL;
378 return $val;
379 }
380
e8e8f3ad 381 /**
382 * Set offset.
383 *
384 * @param string $offset
385 * @param mixed $value
386 */
8adcd073
TO
387 public function offsetSet($offset, $value) {
388 $this->tokenProcessor->rowContexts[$this->tokenRow][$offset] = $value;
389 }
390
e8e8f3ad 391 /**
392 * Unset offset.
393 *
394 * @param mixed $offset
395 */
8adcd073
TO
396 public function offsetUnset($offset) {
397 unset($this->tokenProcessor->rowContexts[$this->tokenRow][$offset]);
398 }
399
e8e8f3ad 400 /**
401 * Get iterator.
402 *
403 * @return \ArrayIterator
404 */
8adcd073
TO
405 public function getIterator() {
406 return new \ArrayIterator($this->createMergedArray());
407 }
408
e8e8f3ad 409 /**
410 * Count.
411 *
412 * @return int
413 */
8adcd073
TO
414 public function count() {
415 return count($this->createMergedArray());
416 }
417
e8e8f3ad 418 /**
419 * Create merged array.
420 *
421 * @return array
422 */
8adcd073
TO
423 protected function createMergedArray() {
424 return array_merge(
425 $this->tokenProcessor->rowContexts[$this->tokenRow],
426 $this->tokenProcessor->context
427 );
428 }
429
430}