Merge pull request #13955 from colemanw/Promise
[civicrm-core.git] / Civi / Token / TokenProcessor.php
CommitLineData
8adcd073
TO
1<?php
2namespace Civi\Token;
3
4use Civi\Token\Event\TokenRegisterEvent;
5use Civi\Token\Event\TokenRenderEvent;
6use Civi\Token\Event\TokenValueEvent;
7use Symfony\Component\EventDispatcher\EventDispatcherInterface;
8use Traversable;
9
10class TokenProcessor {
11
12 /**
13 * @var array
14 * Description of the context in which the tokens are being processed.
15 * Ex: Array('class'=>'CRM_Core_BAO_ActionSchedule', 'schedule' => $dao, 'mapping' => $dao).
16 * Ex: Array('class'=>'CRM_Mailing_BAO_MailingJob', 'mailing' => $dao).
17 *
18 * For lack of a better place, here's a list of known/intended context values:
19 *
20 * - controller: string, the class which is managing the mail-merge.
21 * - smarty: bool, whether to enable smarty support.
22 * - contactId: int, the main person/org discussed in the message.
23 * - contact: array, the main person/org discussed in the message.
24 * (Optional for performance tweaking; if omitted, will load
25 * automatically from contactId.)
26 * - actionSchedule: DAO, the rule which triggered the mailing
27 * [for CRM_Core_BAO_ActionScheduler].
37f37651
AS
28 * - schema: array, a list of fields that will be provided for each row.
29 * This is automatically populated with any general context
30 * keys, but you may need to add extra keys for token-row data.
31 * ex: ['contactId', 'activityId'].
8adcd073
TO
32 */
33 public $context;
34
35 /**
36 * @var EventDispatcherInterface
37 */
38 protected $dispatcher;
39
40 /**
41 * @var array
42 * Each message is an array with keys:
43 * - string: Unprocessed message (eg "Hello, {display_name}.").
44 * - format: Media type (eg "text/plain").
45 * - tokens: List of tokens which are actually used in this message.
46 */
47 protected $messages;
48
49 /**
50 * DO NOT access field this directly. Use TokenRow. This is
51 * marked as public only to benefit TokenRow.
52 *
53 * @var array
54 * Array(int $pos => array $keyValues);
55 */
56 public $rowContexts;
57
58 /**
59 * DO NOT access field this directly. Use TokenRow. This is
60 * marked as public only to benefit TokenRow.
61 *
62 * @var array
63 * Ex: $rowValues[$rowPos][$format][$entity][$field] = 'something';
64 * Ex: $rowValues[3]['text/plain']['contact']['display_name'] = 'something';
65 */
66 public $rowValues;
67
68 /**
69 * A list of available tokens
70 * @var array
71 * Array(string $dottedName => array('entity'=>string, 'field'=>string, 'label'=>string)).
72 */
73 protected $tokens = NULL;
74
1c4a04c9
AS
75 /**
76 * A list of available tokens formatted for display
77 * @var array
78 * Array('{' . $dottedName . '}' => 'labelString')
79 */
80 protected $listTokens = NULL;
81
8adcd073
TO
82 protected $next = 0;
83
84 /**
85 * @param EventDispatcherInterface $dispatcher
86 * @param array $context
87 */
88 public function __construct($dispatcher, $context) {
37f37651
AS
89 $context['schema'] = isset($context['schema'])
90 ? array_unique(array_merge($context['schema'], array_keys($context)))
91 : array_keys($context);
8adcd073
TO
92 $this->dispatcher = $dispatcher;
93 $this->context = $context;
94 }
95
96 /**
97 * Register a string for which we'll need to merge in tokens.
98 *
99 * @param string $name
100 * Ex: 'subject', 'body_html'.
101 * @param string $value
102 * Ex: '<p>Hello {contact.name}</p>'.
103 * @param string $format
104 * Ex: 'text/html'.
4b350175 105 * @return TokenProcessor
8adcd073
TO
106 */
107 public function addMessage($name, $value, $format) {
c64f69d9 108 $this->messages[$name] = [
8adcd073
TO
109 'string' => $value,
110 'format' => $format,
111 'tokens' => \CRM_Utils_Token::getTokens($value),
c64f69d9 112 ];
8adcd073
TO
113 return $this;
114 }
115
116 /**
117 * Add a row of data.
118 *
119 * @return TokenRow
120 */
121 public function addRow() {
122 $key = $this->next++;
c64f69d9
CW
123 $this->rowContexts[$key] = [];
124 $this->rowValues[$key] = [
125 'text/plain' => [],
126 'text/html' => [],
127 ];
8adcd073
TO
128
129 return new TokenRow($this, $key);
130 }
131
132 /**
133 * @param array $params
134 * Array with keys:
135 * - entity: string, e.g. "profile".
136 * - field: string, e.g. "viewUrl".
137 * - label: string, e.g. "Default Profile URL (View Mode)".
4b350175 138 * @return TokenProcessor
8adcd073
TO
139 */
140 public function addToken($params) {
141 $key = $params['entity'] . '.' . $params['field'];
142 $this->tokens[$key] = $params;
143 return $this;
144 }
145
146 /**
147 * @param string $name
148 * @return array
149 * Keys:
150 * - string: Unprocessed message (eg "Hello, {display_name}.").
151 * - format: Media type (eg "text/plain").
152 */
153 public function getMessage($name) {
154 return $this->messages[$name];
155 }
156
157 /**
158 * Get a list of all tokens used in registered messages.
159 *
160 * @return array
161 */
162 public function getMessageTokens() {
c64f69d9 163 $tokens = [];
8adcd073
TO
164 foreach ($this->messages as $message) {
165 $tokens = \CRM_Utils_Array::crmArrayMerge($tokens, $message['tokens']);
166 }
167 foreach (array_keys($tokens) as $e) {
168 $tokens[$e] = array_unique($tokens[$e]);
169 sort($tokens[$e]);
170 }
171 return $tokens;
172 }
173
174 public function getRow($key) {
175 return new TokenRow($this, $key);
176 }
177
178 /**
179 * @return \Traversable<TokenRow>
180 */
181 public function getRows() {
182 return new TokenRowIterator($this, new \ArrayIterator($this->rowContexts));
183 }
184
5d798402
AS
185 /**
186 * Get a list of all unique values for a given context field,
187 * whether defined at the processor or row level.
188 *
189 * @param string $field
190 * Ex: 'contactId'.
191 * @return array
192 * Ex: [12, 34, 56].
193 */
8580ab67 194 public function getContextValues($field, $subfield = NULL) {
5d798402
AS
195 $values = [];
196 if (isset($this->context[$field])) {
8580ab67
AS
197 if ($subfield) {
198 if (isset($this->context[$field]->$subfield)) {
199 $values[] = $this->context[$field]->$subfield;
200 }
201 }
202 else {
203 $values[] = $this->context[$field];
204 }
5d798402
AS
205 }
206 foreach ($this->getRows() as $row) {
207 if (isset($row->context[$field])) {
8580ab67
AS
208 if ($subfield) {
209 if (isset($row->context[$field]->$subfield)) {
210 $values[] = $row->context[$field]->$subfield;
211 }
212 }
213 else {
214 $values[] = $row->context[$field];
215 }
5d798402
AS
216 }
217 }
218 $values = array_unique($values);
219 return $values;
220 }
221
8adcd073
TO
222 /**
223 * Get the list of available tokens.
224 *
225 * @return array
226 * Ex: $tokens['event'] = array('location', 'start_date', 'end_date').
227 */
228 public function getTokens() {
229 if ($this->tokens === NULL) {
c64f69d9
CW
230 $this->tokens = [];
231 $event = new TokenRegisterEvent($this, ['entity' => 'undefined']);
8adcd073
TO
232 $this->dispatcher->dispatch(Events::TOKEN_REGISTER, $event);
233 }
234 return $this->tokens;
235 }
236
1c4a04c9
AS
237 /**
238 * Get the list of available tokens, formatted for display
239 *
240 * @return array
241 * Ex: $tokens[ '{token.name}' ] = "Token label"
242 */
243 public function listTokens() {
244 if ($this->listTokens === NULL) {
c64f69d9 245 $this->listTokens = [];
1c4a04c9
AS
246 foreach ($this->getTokens() as $token => $values) {
247 $this->listTokens['{' . $token . '}'] = $values['label'];
248 }
249 }
250 return $this->listTokens;
251 }
252
8adcd073
TO
253 /**
254 * Compute and store token values.
255 */
256 public function evaluate() {
257 $event = new TokenValueEvent($this);
258 $this->dispatcher->dispatch(Events::TOKEN_EVALUATE, $event);
259 return $this;
260 }
261
262 /**
263 * Render a message.
264 *
265 * @param string $name
266 * The name previously registered with addMessage().
267 * @param TokenRow|int $row
268 * The object or ID for the row previously registered with addRow().
269 * @return string
270 * Fully rendered message, with tokens merged.
271 */
272 public function render($name, $row) {
273 if (!is_object($row)) {
274 $row = $this->getRow($row);
275 }
276
277 $message = $this->getMessage($name);
278 $row->fill($message['format']);
279 $useSmarty = !empty($row->context['smarty']);
280
281 // FIXME preg_callback.
282 $tokens = $this->rowValues[$row->tokenRow][$message['format']];
c64f69d9 283 $flatTokens = [];
8adcd073 284 \CRM_Utils_Array::flatten($tokens, $flatTokens, '', '.');
c64f69d9 285 $filteredTokens = [];
8adcd073
TO
286 foreach ($flatTokens as $k => $v) {
287 $filteredTokens['{' . $k . '}'] = ($useSmarty ? \CRM_Utils_Token::tokenEscapeSmarty($v) : $v);
288 }
289
290 $event = new TokenRenderEvent($this);
291 $event->message = $message;
292 $event->context = $row->context;
293 $event->row = $row;
294 $event->string = strtr($message['string'], $filteredTokens);
295 $this->dispatcher->dispatch(Events::TOKEN_RENDER, $event);
296 return $event->string;
297 }
298
299}
300
301class TokenRowIterator extends \IteratorIterator {
302
303 protected $tokenProcessor;
304
305 /**
306 * @param TokenProcessor $tokenProcessor
307 * @param Traversable $iterator
308 */
309 public function __construct(TokenProcessor $tokenProcessor, Traversable $iterator) {
310 parent::__construct($iterator); // TODO: Change the autogenerated stub
311 $this->tokenProcessor = $tokenProcessor;
312 }
313
314 public function current() {
315 return new TokenRow($this->tokenProcessor, parent::key());
316 }
317
318}