8 * A TokenRow is a helper/stub providing simplified access to the TokenProcessor.
9 * There are two common cases for using the TokenRow stub:
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:
15 * $proc->addRow()->context('contact_id', 123);
16 * $proc->addRow()->context('contact_id', 456);
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`):
23 * foreach ($proc->getRows() as $row) {
24 * $row->tokens('profile', [
25 * 'viewUrl' => 'http://example.com/profile?cid=' . urlencode($row->context['contact_id'];
30 * The context and tokens can be accessed using either methods or attributes.
33 * # Setting context data
34 * $row->context('contact_id', 123);
35 * $row->context(['contact_id' => 123]);
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');
41 * # Reading context data
42 * echo $row->context['contact_id'];
44 * # Reading token data
45 * echo $row->tokens['profile']['viewUrl'];
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)`).
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.
60 * The token-processor is where most data is actually stored.
62 * Note: Not intended for public usage. However, this is marked public to allow
63 * interaction classes in this package (`TokenProcessor`<=>`TokenRow`<=>`TokenRowContext`).
67 public $tokenProcessor;
70 * Row ID - the record within TokenProcessor that we're accessing.
77 * The MIME type associated with new token-values.
79 * This is generally manipulated as part of a fluent chain, eg
81 * $row->format('text/plain')->token(['display_name', 'Alice Bobdaughter']);
88 * @var array|\ArrayAccess
89 * List of token values.
90 * This is a facade for the TokenProcessor::$rowValues.
91 * Ex: ['contact' => ['display_name' => 'Alice']]
96 * @var array|\ArrayAccess
97 * List of context values.
98 * This is a facade for the TokenProcessor::$rowContexts.
99 * Ex: ['controller' => 'CRM_Foo_Bar']
103 public function __construct(TokenProcessor
$tokenProcessor, $key) {
104 $this->tokenProcessor
= $tokenProcessor;
105 $this->tokenRow
= $key;
107 $this->format('text/plain');
108 $this->context
= new TokenRowContext($tokenProcessor, $key);
112 * @param string $format
115 public function format($format) {
116 $this->format
= $format;
117 $this->tokens
= &$this->tokenProcessor
->rowValues
[$this->tokenRow
][$format];
122 * Update the value of a context element.
124 * @param string|array $a
128 public function context($a = NULL, $b = NULL) {
130 \CRM_Utils_Array
::extend($this->tokenProcessor
->rowContexts
[$this->tokenRow
], $a);
132 elseif (is_array($b)) {
133 \CRM_Utils_Array
::extend($this->tokenProcessor
->rowContexts
[$this->tokenRow
][$a], $b);
136 $this->tokenProcessor
->rowContexts
[$this->tokenRow
][$a] = $b;
142 * Update the value of a token.
144 * @param string|array $a
145 * @param string|array $b
149 public function tokens($a = NULL, $b = NULL, $c = NULL) {
151 \CRM_Utils_Array
::extend($this->tokens
, $a);
153 elseif (is_array($b)) {
154 \CRM_Utils_Array
::extend($this->tokens
[$a], $b);
156 elseif (is_array($c)) {
157 \CRM_Utils_Array
::extend($this->tokens
[$a][$b], $c);
159 elseif ($c === NULL) {
160 $this->tokens
[$a] = $b;
163 $this->tokens
[$a][$b] = $c;
169 * Update the value of a custom field token.
171 * @param string $entity
172 * @param int $customFieldID
173 * @param int $entityID
176 public function customToken($entity, $customFieldID, $entityID) {
177 $customFieldName = "custom_" . $customFieldID;
178 $record = civicrm_api3($entity, "getSingle", [
179 'return' => $customFieldName,
182 $fieldValue = \CRM_Utils_Array
::value($customFieldName, $record, '');
184 // format the raw custom field value into proper display value
185 if (isset($fieldValue)) {
186 $fieldValue = \CRM_Core_BAO_CustomField
::displayValue($fieldValue, $customFieldID);
189 return $this->tokens($entity, $customFieldName, $fieldValue);
193 * Update the value of a token. Apply formatting based on DB schema.
195 * @param string $tokenEntity
196 * @param string $tokenField
197 * @param string $baoName
198 * @param string $baoField
199 * @param mixed $fieldValue
201 * @throws \CRM_Core_Exception
203 public function dbToken($tokenEntity, $tokenField, $baoName, $baoField, $fieldValue) {
204 \CRM_Core_Error
::deprecatedFunctionWarning('no alternative');
205 if ($fieldValue === NULL ||
$fieldValue === '') {
206 return $this->tokens($tokenEntity, $tokenField, '');
209 $fields = $baoName::fields();
210 if (!empty($fields[$baoField]['pseudoconstant'])) {
211 $options = $baoName::buildOptions($baoField, 'get');
212 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, $options[$fieldValue]);
215 switch ($fields[$baoField]['type']) {
216 case \CRM_Utils_Type
::T_DATE + \CRM_Utils_Type
::T_TIME
:
217 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, \CRM_Utils_Date
::customFormat($fieldValue));
219 case \CRM_Utils_Type
::T_MONEY
:
220 // Is this something you should ever use? Seems like you need more context
221 // to know which currency to use.
222 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, \CRM_Utils_Money
::format($fieldValue));
224 case \CRM_Utils_Type
::T_STRING
:
225 case \CRM_Utils_Type
::T_BOOLEAN
:
226 case \CRM_Utils_Type
::T_INT
:
227 case \CRM_Utils_Type
::T_TEXT
:
228 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, $fieldValue);
232 throw new \
CRM_Core_Exception("Cannot format token for field '$baoField' in '$baoName'");
236 * Auto-convert between different formats
238 * @param string $format
242 public function fill($format = NULL) {
243 if ($format === NULL) {
244 $format = $this->format
;
247 if (!isset($this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/html'])) {
248 $this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/html'] = [];
250 if (!isset($this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/plain'])) {
251 $this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/plain'] = [];
254 $htmlTokens = &$this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/html'];
255 $textTokens = &$this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/plain'];
260 foreach ($textTokens as $entity => $values) {
261 $entityFields = civicrm_api3($entity, "getFields", ['api_action' => 'get']);
262 foreach ($values as $field => $value) {
263 if (!isset($htmlTokens[$entity][$field])) {
264 // CRM-18420 - Activity Details Field are enclosed within <p>,
265 // hence if $body_text is empty, htmlentities will lead to
266 // conversion of these tags resulting in raw HTML.
267 if ($entity == 'activity' && $field == 'details') {
268 $htmlTokens[$entity][$field] = $value;
270 elseif (\CRM_Utils_Array
::value('data_type', \CRM_Utils_Array
::value($field, $entityFields['values'])) == 'Memo') {
271 // Memo fields aka custom fields of type Note are html.
272 $htmlTokens[$entity][$field] = \CRM_Utils_String
::purifyHTML($value);
275 $htmlTokens[$entity][$field] = is_object($value) ?
$value : htmlentities($value);
284 foreach ($htmlTokens as $entity => $values) {
285 foreach ($values as $field => $value) {
286 if (!$value instanceof \DateTime
) {
287 $value = html_entity_decode(strip_tags($value));
289 if (!isset($textTokens[$entity][$field])) {
290 $textTokens[$entity][$field] = $value;
297 throw new \
RuntimeException('Invalid format');
306 * @param string $name
307 * The name previously registered with TokenProcessor::addMessage.
309 * Fully rendered message, with tokens merged.
311 public function render($name) {
312 return $this->tokenProcessor
->render($name, $this);
318 * Class TokenRowContext
319 * @package Civi\Token
321 * Combine the row-context and general-context into a single array-like facade.
323 class TokenRowContext
implements \ArrayAccess
, \IteratorAggregate
, \Countable
{
326 * @var TokenProcessor
328 protected $tokenProcessor;
335 * @param array $tokenProcessor
336 * @param array $tokenRow
338 public function __construct($tokenProcessor, $tokenRow) {
339 $this->tokenProcessor
= $tokenProcessor;
340 $this->tokenRow
= $tokenRow;
346 * @param mixed $offset
350 public function offsetExists($offset) {
351 return isset($this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset])
352 ||
isset($this->tokenProcessor
->context
[$offset]);
358 * @param string $offset
362 public function &offsetGet($offset) {
363 if (isset($this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset])) {
364 return $this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset];
366 if (isset($this->tokenProcessor
->context
[$offset])) {
367 return $this->tokenProcessor
->context
[$offset];
376 * @param string $offset
377 * @param mixed $value
379 public function offsetSet($offset, $value) {
380 $this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset] = $value;
386 * @param mixed $offset
388 public function offsetUnset($offset) {
389 unset($this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset]);
395 * @return \ArrayIterator
397 public function getIterator() {
398 return new \
ArrayIterator($this->createMergedArray());
406 public function count() {
407 return count($this->createMergedArray());
411 * Create merged array.
415 protected function createMergedArray() {
417 $this->tokenProcessor
->rowContexts
[$this->tokenRow
],
418 $this->tokenProcessor
->context