8 * A TokenRow is a helper providing simplified access to the
11 * A TokenRow combines two elements:
12 * - context: This is backend data provided by the controller.
13 * - tokens: This is frontend data that can be mail-merged.
15 * The context and tokens can be accessed using either methods
16 * or attributes. The methods are appropriate for updates
17 * (and generally accept a mix of arrays), and the attributes
18 * are appropriate for reads.
20 * To update the context or the tokens, use the methods.
21 * Note that the methods are fairly flexible about accepting
22 * single values or arrays. If given an array, the values
23 * will be merged recursively.
27 * ->context('contact_id', 123)
28 * ->context(array('contact_id' => 123))
29 * ->tokens('profile', array('viewUrl' => 'http://example.com'))
30 * ->tokens('profile', 'viewUrl, 'http://example.com');
32 * echo $row->context['contact_id'];
33 * echo $row->tokens['profile']['viewUrl'];
35 * $row->tokens('profile', array(
36 * 'viewUrl' => 'http://example.com/view/' . urlencode($row->context['contact_id'];
45 public $tokenProcessor;
52 * @var array|\ArrayAccess
53 * List of token values.
54 * Ex: array('contact' => array('display_name' => 'Alice')).
59 * @var array|\ArrayAccess
60 * List of context values.
61 * Ex: array('controller' => 'CRM_Foo_Bar').
65 public function __construct(TokenProcessor
$tokenProcessor, $key) {
66 $this->tokenProcessor
= $tokenProcessor;
67 $this->tokenRow
= $key;
68 $this->format('text/plain'); // Set a default.
69 $this->context
= new TokenRowContext($tokenProcessor, $key);
73 * @param string $format
76 public function format($format) {
77 $this->format
= $format;
78 $this->tokens
= &$this->tokenProcessor
->rowValues
[$this->tokenRow
][$format];
83 * Update the value of a context element.
85 * @param string|array $a
89 public function context($a = NULL, $b = NULL) {
91 \CRM_Utils_Array
::extend($this->tokenProcessor
->rowContexts
[$this->tokenRow
], $a);
93 elseif (is_array($b)) {
94 \CRM_Utils_Array
::extend($this->tokenProcessor
->rowContexts
[$this->tokenRow
][$a], $b);
97 $this->tokenProcessor
->rowContexts
[$this->tokenRow
][$a] = $b;
103 * Update the value of a token.
105 * @param string|array $a
106 * @param string|array $b
110 public function tokens($a = NULL, $b = NULL, $c = NULL) {
112 \CRM_Utils_Array
::extend($this->tokens
, $a);
114 elseif (is_array($b)) {
115 \CRM_Utils_Array
::extend($this->tokens
[$a], $b);
117 elseif (is_array($c)) {
118 \CRM_Utils_Array
::extend($this->tokens
[$a][$b], $c);
120 elseif ($c === NULL) {
121 $this->tokens
[$a] = $b;
124 $this->tokens
[$a][$b] = $c;
130 * Update the value of a custom field token.
132 * @param string $entity
133 * @param int $customFieldID
134 * @param int $entityID
137 public function customToken($entity, $customFieldID, $entityID) {
138 $customFieldName = "custom_" . $customFieldID;
139 $record = civicrm_api3($entity, "getSingle", [
140 'return' => $customFieldName,
143 $fieldValue = \CRM_Utils_Array
::value($customFieldName, $record, '');
145 // format the raw custom field value into proper display value
146 if (isset($fieldValue)) {
147 $fieldValue = \CRM_Core_BAO_CustomField
::displayValue($fieldValue, $customFieldID);
150 return $this->tokens($entity, $customFieldName, $fieldValue);
154 * Update the value of a token. Apply formatting based on DB schema.
156 * @param string $tokenEntity
157 * @param string $tokenField
158 * @param string $baoName
159 * @param array $baoField
160 * @param mixed $fieldValue
162 * @throws \CRM_Core_Exception
164 public function dbToken($tokenEntity, $tokenField, $baoName, $baoField, $fieldValue) {
165 if ($fieldValue === NULL ||
$fieldValue === '') {
166 return $this->tokens($tokenEntity, $tokenField, '');
169 $fields = $baoName::fields();
170 if (!empty($fields[$baoField]['pseudoconstant'])) {
171 $options = $baoName::buildOptions($baoField, 'get');
172 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, $options[$fieldValue]);
175 switch ($fields[$baoField]['type']) {
176 case \CRM_Utils_Type
::T_DATE + \CRM_Utils_Type
::T_TIME
:
177 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, \CRM_Utils_Date
::customFormat($fieldValue));
179 case \CRM_Utils_Type
::T_MONEY
:
180 // Is this something you should ever use? Seems like you need more context
181 // to know which currency to use.
182 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, \CRM_Utils_Money
::format($fieldValue));
184 case \CRM_Utils_Type
::T_STRING
:
185 case \CRM_Utils_Type
::T_BOOLEAN
:
186 case \CRM_Utils_Type
::T_INT
:
187 case \CRM_Utils_Type
::T_TEXT
:
188 return $this->format('text/plain')->tokens($tokenEntity, $tokenField, $fieldValue);
192 throw new \
CRM_Core_Exception("Cannot format token for field '$baoField' in '$baoName'");
196 * Auto-convert between different formats
198 * @param string $format
202 public function fill($format = NULL) {
203 if ($format === NULL) {
204 $format = $this->format
;
207 if (!isset($this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/html'])) {
208 $this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/html'] = [];
210 if (!isset($this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/plain'])) {
211 $this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/plain'] = [];
214 $htmlTokens = &$this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/html'];
215 $textTokens = &$this->tokenProcessor
->rowValues
[$this->tokenRow
]['text/plain'];
220 foreach ($textTokens as $entity => $values) {
221 $entityFields = civicrm_api3($entity, "getFields", ['api_action' => 'get']);
222 foreach ($values as $field => $value) {
223 if (!isset($htmlTokens[$entity][$field])) {
224 // CRM-18420 - Activity Details Field are enclosed within <p>,
225 // hence if $body_text is empty, htmlentities will lead to
226 // conversion of these tags resulting in raw HTML.
227 if ($entity == 'activity' && $field == 'details') {
228 $htmlTokens[$entity][$field] = $value;
230 elseif (\CRM_Utils_Array
::value('data_type', \CRM_Utils_Array
::value($field, $entityFields['values'])) == 'Memo') {
231 // Memo fields aka custom fields of type Note are html.
232 $htmlTokens[$entity][$field] = CRM_Utils_String
::purifyHTML($value);
235 $htmlTokens[$entity][$field] = htmlentities($value);
244 foreach ($htmlTokens as $entity => $values) {
245 foreach ($values as $field => $value) {
246 if (!isset($textTokens[$entity][$field])) {
247 $textTokens[$entity][$field] = html_entity_decode(strip_tags($value));
254 throw new \
RuntimeException("Invalid format");
263 * @param string $name
264 * The name previously registered with TokenProcessor::addMessage.
266 * Fully rendered message, with tokens merged.
268 public function render($name) {
269 return $this->tokenProcessor
->render($name, $this);
275 * Class TokenRowContext
276 * @package Civi\Token
278 * Combine the row-context and general-context into a single array-like facade.
280 class TokenRowContext
implements \ArrayAccess
, \IteratorAggregate
, \Countable
{
283 * @var TokenProcessor
285 protected $tokenProcessor;
292 * @param array $tokenProcessor
293 * @param array $tokenRow
295 public function __construct($tokenProcessor, $tokenRow) {
296 $this->tokenProcessor
= $tokenProcessor;
297 $this->tokenRow
= $tokenRow;
303 * @param mixed $offset
307 public function offsetExists($offset) {
309 isset($this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset])
310 ||
isset($this->tokenProcessor
->context
[$offset]);
316 * @param string $offset
320 public function &offsetGet($offset) {
321 if (isset($this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset])) {
322 return $this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset];
324 if (isset($this->tokenProcessor
->context
[$offset])) {
325 return $this->tokenProcessor
->context
[$offset];
334 * @param string $offset
335 * @param mixed $value
337 public function offsetSet($offset, $value) {
338 $this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset] = $value;
344 * @param mixed $offset
346 public function offsetUnset($offset) {
347 unset($this->tokenProcessor
->rowContexts
[$this->tokenRow
][$offset]);
353 * @return \ArrayIterator
355 public function getIterator() {
356 return new \
ArrayIterator($this->createMergedArray());
364 public function count() {
365 return count($this->createMergedArray());
369 * Create merged array.
373 protected function createMergedArray() {
375 $this->tokenProcessor
->rowContexts
[$this->tokenRow
],
376 $this->tokenProcessor
->context