Add 'schema' to \Civi\Token\TokenProcessor()
[civicrm-core.git] / Civi / Token / TokenProcessor.php
1 <?php
2 namespace Civi\Token;
3
4 use Civi\Token\Event\TokenRegisterEvent;
5 use Civi\Token\Event\TokenRenderEvent;
6 use Civi\Token\Event\TokenValueEvent;
7 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
8 use Traversable;
9
10 class 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].
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'].
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
75 protected $next = 0;
76
77 /**
78 * @param EventDispatcherInterface $dispatcher
79 * @param array $context
80 */
81 public function __construct($dispatcher, $context) {
82 $context['schema'] = isset($context['schema'])
83 ? array_unique(array_merge($context['schema'], array_keys($context)))
84 : array_keys($context);
85 $this->dispatcher = $dispatcher;
86 $this->context = $context;
87 }
88
89 /**
90 * Register a string for which we'll need to merge in tokens.
91 *
92 * @param string $name
93 * Ex: 'subject', 'body_html'.
94 * @param string $value
95 * Ex: '<p>Hello {contact.name}</p>'.
96 * @param string $format
97 * Ex: 'text/html'.
98 * @return TokenProcessor
99 */
100 public function addMessage($name, $value, $format) {
101 $this->messages[$name] = array(
102 'string' => $value,
103 'format' => $format,
104 'tokens' => \CRM_Utils_Token::getTokens($value),
105 );
106 return $this;
107 }
108
109 /**
110 * Add a row of data.
111 *
112 * @return TokenRow
113 */
114 public function addRow() {
115 $key = $this->next++;
116 $this->rowContexts[$key] = array();
117 $this->rowValues[$key] = array(
118 'text/plain' => array(),
119 'text/html' => array(),
120 );
121
122 return new TokenRow($this, $key);
123 }
124
125 /**
126 * @param array $params
127 * Array with keys:
128 * - entity: string, e.g. "profile".
129 * - field: string, e.g. "viewUrl".
130 * - label: string, e.g. "Default Profile URL (View Mode)".
131 * @return TokenProcessor
132 */
133 public function addToken($params) {
134 $key = $params['entity'] . '.' . $params['field'];
135 $this->tokens[$key] = $params;
136 return $this;
137 }
138
139 /**
140 * @param string $name
141 * @return array
142 * Keys:
143 * - string: Unprocessed message (eg "Hello, {display_name}.").
144 * - format: Media type (eg "text/plain").
145 */
146 public function getMessage($name) {
147 return $this->messages[$name];
148 }
149
150 /**
151 * Get a list of all tokens used in registered messages.
152 *
153 * @return array
154 */
155 public function getMessageTokens() {
156 $tokens = array();
157 foreach ($this->messages as $message) {
158 $tokens = \CRM_Utils_Array::crmArrayMerge($tokens, $message['tokens']);
159 }
160 foreach (array_keys($tokens) as $e) {
161 $tokens[$e] = array_unique($tokens[$e]);
162 sort($tokens[$e]);
163 }
164 return $tokens;
165 }
166
167 public function getRow($key) {
168 return new TokenRow($this, $key);
169 }
170
171 /**
172 * @return \Traversable<TokenRow>
173 */
174 public function getRows() {
175 return new TokenRowIterator($this, new \ArrayIterator($this->rowContexts));
176 }
177
178 /**
179 * Get a list of all unique values for a given context field,
180 * whether defined at the processor or row level.
181 *
182 * @param string $field
183 * Ex: 'contactId'.
184 * @return array
185 * Ex: [12, 34, 56].
186 */
187 public function getContextValues($field, $subfield = NULL) {
188 $values = [];
189 if (isset($this->context[$field])) {
190 if ($subfield) {
191 if (isset($this->context[$field]->$subfield)) {
192 $values[] = $this->context[$field]->$subfield;
193 }
194 }
195 else {
196 $values[] = $this->context[$field];
197 }
198 }
199 foreach ($this->getRows() as $row) {
200 if (isset($row->context[$field])) {
201 if ($subfield) {
202 if (isset($row->context[$field]->$subfield)) {
203 $values[] = $row->context[$field]->$subfield;
204 }
205 }
206 else {
207 $values[] = $row->context[$field];
208 }
209 }
210 }
211 $values = array_unique($values);
212 return $values;
213 }
214
215 /**
216 * Get the list of available tokens.
217 *
218 * @return array
219 * Ex: $tokens['event'] = array('location', 'start_date', 'end_date').
220 */
221 public function getTokens() {
222 if ($this->tokens === NULL) {
223 $this->tokens = array();
224 $event = new TokenRegisterEvent($this, array('entity' => 'undefined'));
225 $this->dispatcher->dispatch(Events::TOKEN_REGISTER, $event);
226 }
227 return $this->tokens;
228 }
229
230 /**
231 * Compute and store token values.
232 */
233 public function evaluate() {
234 $event = new TokenValueEvent($this);
235 $this->dispatcher->dispatch(Events::TOKEN_EVALUATE, $event);
236 return $this;
237 }
238
239 /**
240 * Render a message.
241 *
242 * @param string $name
243 * The name previously registered with addMessage().
244 * @param TokenRow|int $row
245 * The object or ID for the row previously registered with addRow().
246 * @return string
247 * Fully rendered message, with tokens merged.
248 */
249 public function render($name, $row) {
250 if (!is_object($row)) {
251 $row = $this->getRow($row);
252 }
253
254 $message = $this->getMessage($name);
255 $row->fill($message['format']);
256 $useSmarty = !empty($row->context['smarty']);
257
258 // FIXME preg_callback.
259 $tokens = $this->rowValues[$row->tokenRow][$message['format']];
260 $flatTokens = array();
261 \CRM_Utils_Array::flatten($tokens, $flatTokens, '', '.');
262 $filteredTokens = array();
263 foreach ($flatTokens as $k => $v) {
264 $filteredTokens['{' . $k . '}'] = ($useSmarty ? \CRM_Utils_Token::tokenEscapeSmarty($v) : $v);
265 }
266
267 $event = new TokenRenderEvent($this);
268 $event->message = $message;
269 $event->context = $row->context;
270 $event->row = $row;
271 $event->string = strtr($message['string'], $filteredTokens);
272 $this->dispatcher->dispatch(Events::TOKEN_RENDER, $event);
273 return $event->string;
274 }
275
276 }
277
278 class TokenRowIterator extends \IteratorIterator {
279
280 protected $tokenProcessor;
281
282 /**
283 * @param TokenProcessor $tokenProcessor
284 * @param Traversable $iterator
285 */
286 public function __construct(TokenProcessor $tokenProcessor, Traversable $iterator) {
287 parent::__construct($iterator); // TODO: Change the autogenerated stub
288 $this->tokenProcessor = $tokenProcessor;
289 }
290
291 public function current() {
292 return new TokenRow($this->tokenProcessor, parent::key());
293 }
294
295 }