4 use Civi\Token\Event\TokenRegisterEvent
;
5 use Civi\Token\Event\TokenRenderEvent
;
6 use Civi\Token\Event\TokenValueEvent
;
10 * The TokenProcessor is a template/token-engine. It is heavily influenced by
11 * traditional expectations of CiviMail, but it's adapted to an object-oriented,
16 * The CiviMail heritage gives the following expectations:
18 * - Messages are often composed of multiple parts (e.g. HTML-part, text-part, and subject-part).
19 * - Messages are often composed in batches for multiple recipients.
20 * - Tokens are denoted as `{foo.bar}`.
21 * - Data should be loaded in an optimized fashion - fetch only the needed
22 * columns, and fetch them with one query (per-table).
24 * The question of "optimized" data-loading is a key differentiator/complication.
25 * This requires some kind of communication/integration between the template-parser and data-loader.
29 * There are generally two perspectives on using TokenProcessor:
31 * 1. Composing messages: You need to specify the template contents (eg `addMessage(...)`)
32 * and the recipients' key data (eg `addRow(['contact_id' => 123])`).
33 * 2. Defining tokens/entities/data-loaders: You need to listen for TokenProcessor
34 * events; if any of your tokens/entities are used, then load the batch of data.
36 * Each use-case is presented with examples in the Developer Guide:
38 * @link https://docs.civicrm.org/dev/en/latest/framework/token/
40 class TokenProcessor
{
44 * Description of the context in which the tokens are being processed.
45 * Ex: Array('class'=>'CRM_Core_BAO_ActionSchedule', 'schedule' => $dao, 'mapping' => $dao).
46 * Ex: Array('class'=>'CRM_Mailing_BAO_MailingJob', 'mailing' => $dao).
48 * For lack of a better place, here's a list of known/intended context values:
50 * - controller: string, the class which is managing the mail-merge.
51 * - smarty: bool, whether to enable smarty support.
52 * - contactId: int, the main person/org discussed in the message.
53 * - contact: array, the main person/org discussed in the message.
54 * (Optional for performance tweaking; if omitted, will load
55 * automatically from contactId.)
56 * - actionSchedule: DAO, the rule which triggered the mailing
57 * [for CRM_Core_BAO_ActionScheduler].
58 * - schema: array, a list of fields that will be provided for each row.
59 * This is automatically populated with any general context
60 * keys, but you may need to add extra keys for token-row data.
61 * ex: ['contactId', 'activity_id']. (Note we are standardising on the latter).
66 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
68 protected $dispatcher;
72 * Each message is an array with keys:
73 * - string: Unprocessed message (eg "Hello, {display_name}.").
74 * - format: Media type (eg "text/plain").
75 * - tokens: List of tokens which are actually used in this message.
80 * DO NOT access field this directly. Use TokenRow. This is
81 * marked as public only to benefit TokenRow.
84 * Array(int $pos => array $keyValues);
89 * DO NOT access field this directly. Use TokenRow. This is
90 * marked as public only to benefit TokenRow.
93 * Ex: $rowValues[$rowPos][$format][$entity][$field] = 'something';
94 * Ex: $rowValues[3]['text/plain']['contact']['display_name'] = 'something';
99 * A list of available tokens
101 * Array(string $dottedName => array('entity'=>string, 'field'=>string, 'label'=>string)).
103 protected $tokens = NULL;
106 * A list of available tokens formatted for display
108 * Array('{' . $dottedName . '}' => 'labelString')
110 protected $listTokens = NULL;
115 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
116 * @param array $context
118 public function __construct($dispatcher, $context) {
119 $context['schema'] = isset($context['schema'])
120 ?
array_unique(array_merge($context['schema'], array_keys($context)))
121 : array_keys($context);
122 $this->dispatcher
= $dispatcher;
123 $this->context
= $context;
127 * Register a string for which we'll need to merge in tokens.
129 * @param string $name
130 * Ex: 'subject', 'body_html'.
131 * @param string $value
132 * Ex: '<p>Hello {contact.name}</p>'.
133 * @param string $format
135 * @return TokenProcessor
137 public function addMessage($name, $value, $format) {
138 $this->messages
[$name] = [
141 'tokens' => \CRM_Utils_Token
::getTokens($value),
149 * @param array|NULL $context
150 * Optionally, initialize the context for this row.
151 * Ex: ['contact_id' => 123].
154 public function addRow($context = NULL) {
155 $key = $this->next++
;
156 $this->rowContexts
[$key] = [];
157 $this->rowValues
[$key] = [
162 $row = new TokenRow($this, $key);
163 if ($context !== NULL) {
164 $row->context($context);
172 * @param array $contexts
173 * List of rows to add.
174 * Ex: [['contact_id'=>123], ['contact_id'=>456]]
176 * List of row objects
178 public function addRows($contexts) {
180 foreach ($contexts as $context) {
181 $row = $this->addRow($context);
182 $rows[$row->tokenRow
] = $row;
188 * @param array $params
190 * - entity: string, e.g. "profile".
191 * - field: string, e.g. "viewUrl".
192 * - label: string, e.g. "Default Profile URL (View Mode)".
193 * @return TokenProcessor
195 public function addToken($params) {
196 $key = $params['entity'] . '.' . $params['field'];
197 $this->tokens
[$key] = $params;
202 * @param string $name
205 * - string: Unprocessed message (eg "Hello, {display_name}.").
206 * - format: Media type (eg "text/plain").
208 public function getMessage($name) {
209 return $this->messages
[$name];
213 * Get a list of all tokens used in registered messages.
216 * The list of activated tokens, indexed by object/entity.
217 * Array(string $entityName => string[] $fieldNames)
219 * Ex: If a message says 'Hello {contact.first_name} {contact.last_name}!',
220 * then $result['contact'] would be ['first_name', 'last_name'].
222 public function getMessageTokens() {
224 foreach ($this->messages
as $message) {
225 $tokens = \CRM_Utils_Array
::crmArrayMerge($tokens, $message['tokens']);
227 foreach (array_keys($tokens) as $e) {
228 $tokens[$e] = array_unique($tokens[$e]);
235 * Get a specific row (i.e. target or recipient).
237 * Ex: echo $p->getRow(2)->context['contact_id'];
238 * Ex: $p->getRow(3)->token('profile', 'viewUrl', 'http://example.com/profile?cid=3');
242 * @return \Civi\Token\TokenRow
243 * The row is presented with a fluent, OOP facade.
246 public function getRow($key) {
247 return new TokenRow($this, $key);
251 * Get the list of rows (i.e. targets/recipients to generate).
254 * @return \Traversable<TokenRow>
255 * Each row is presented with a fluent, OOP facade.
257 public function getRows() {
258 return new TokenRowIterator($this, new \
ArrayIterator($this->rowContexts
));
262 * Get a list of all unique values for a given context field,
263 * whether defined at the processor or row level.
265 * @param string $field
267 * @param string|NULL $subfield
271 public function getContextValues($field, $subfield = NULL) {
273 if (isset($this->context
[$field])) {
275 if (isset($this->context
[$field]->$subfield)) {
276 $values[] = $this->context
[$field]->$subfield;
280 $values[] = $this->context
[$field];
283 foreach ($this->getRows() as $row) {
284 if (isset($row->context
[$field])) {
286 if (isset($row->context
[$field]->$subfield)) {
287 $values[] = $row->context
[$field]->$subfield;
291 $values[] = $row->context
[$field];
295 $values = array_unique($values);
300 * Get the list of available tokens.
303 * Ex: $tokens['event'] = ['location', 'start_date', 'end_date'].
305 public function getTokens() {
306 if ($this->tokens
=== NULL) {
308 $event = new TokenRegisterEvent($this, ['entity' => 'undefined']);
309 $this->dispatcher
->dispatch('civi.token.list', $event);
311 return $this->tokens
;
315 * Get the list of available tokens, formatted for display
318 * Ex: $tokens['{token.name}'] = "Token label"
320 public function listTokens() {
321 if ($this->listTokens
=== NULL) {
322 $this->listTokens
= [];
323 foreach ($this->getTokens() as $token => $values) {
324 $this->listTokens
['{' . $token . '}'] = $values['label'];
327 return $this->listTokens
;
331 * Compute and store token values.
333 public function evaluate() {
334 $event = new TokenValueEvent($this);
335 $this->dispatcher
->dispatch('civi.token.eval', $event);
342 * @param string $name
343 * The name previously registered with addMessage().
344 * @param TokenRow|int $row
345 * The object or ID for the row previously registered with addRow().
347 * Fully rendered message, with tokens merged.
349 public function render($name, $row) {
350 if (!is_object($row)) {
351 $row = $this->getRow($row);
354 $message = $this->getMessage($name);
355 $row->fill($message['format']);
356 $useSmarty = !empty($row->context
['smarty']);
359 *@FIXME preg_callback.
361 $tokens = $this->rowValues
[$row->tokenRow
][$message['format']];
363 \CRM_Utils_Array
::flatten($tokens, $flatTokens, '', '.');
364 $filteredTokens = [];
365 foreach ($flatTokens as $k => $v) {
366 $filteredTokens['{' . $k . '}'] = ($useSmarty ? \CRM_Utils_Token
::tokenEscapeSmarty($v) : $v);
369 $event = new TokenRenderEvent($this);
370 $event->message
= $message;
371 $event->context
= $row->context
;
373 $event->string = strtr($message['string'], $filteredTokens);
374 $this->dispatcher
->dispatch('civi.token.render', $event);
375 return $event->string;
380 class TokenRowIterator
extends \IteratorIterator
{
382 protected $tokenProcessor;
385 * @param TokenProcessor $tokenProcessor
386 * @param \Traversable $iterator
388 public function __construct(TokenProcessor
$tokenProcessor, Traversable
$iterator) {
389 // TODO: Change the autogenerated stub
390 parent
::__construct($iterator);
391 $this->tokenProcessor
= $tokenProcessor;
394 public function current() {
395 return new TokenRow($this->tokenProcessor
, parent
::key());