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