CRM-20336: Failed contributions should be set as failed, not left as pending (more...
[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].
28 */
29 public $context;
30
31 /**
32 * @var EventDispatcherInterface
33 */
34 protected $dispatcher;
35
36 /**
37 * @var array
38 * Each message is an array with keys:
39 * - string: Unprocessed message (eg "Hello, {display_name}.").
40 * - format: Media type (eg "text/plain").
41 * - tokens: List of tokens which are actually used in this message.
42 */
43 protected $messages;
44
45 /**
46 * DO NOT access field this directly. Use TokenRow. This is
47 * marked as public only to benefit TokenRow.
48 *
49 * @var array
50 * Array(int $pos => array $keyValues);
51 */
52 public $rowContexts;
53
54 /**
55 * DO NOT access field this directly. Use TokenRow. This is
56 * marked as public only to benefit TokenRow.
57 *
58 * @var array
59 * Ex: $rowValues[$rowPos][$format][$entity][$field] = 'something';
60 * Ex: $rowValues[3]['text/plain']['contact']['display_name'] = 'something';
61 */
62 public $rowValues;
63
64 /**
65 * A list of available tokens
66 * @var array
67 * Array(string $dottedName => array('entity'=>string, 'field'=>string, 'label'=>string)).
68 */
69 protected $tokens = NULL;
70
71 protected $next = 0;
72
73 /**
74 * @param EventDispatcherInterface $dispatcher
75 * @param array $context
76 */
77 public function __construct($dispatcher, $context) {
78 $this->dispatcher = $dispatcher;
79 $this->context = $context;
80 }
81
82 /**
83 * Register a string for which we'll need to merge in tokens.
84 *
85 * @param string $name
86 * Ex: 'subject', 'body_html'.
87 * @param string $value
88 * Ex: '<p>Hello {contact.name}</p>'.
89 * @param string $format
90 * Ex: 'text/html'.
4b350175 91 * @return TokenProcessor
8adcd073
TO
92 */
93 public function addMessage($name, $value, $format) {
94 $this->messages[$name] = array(
95 'string' => $value,
96 'format' => $format,
97 'tokens' => \CRM_Utils_Token::getTokens($value),
98 );
99 return $this;
100 }
101
102 /**
103 * Add a row of data.
104 *
105 * @return TokenRow
106 */
107 public function addRow() {
108 $key = $this->next++;
109 $this->rowContexts[$key] = array();
110 $this->rowValues[$key] = array(
111 'text/plain' => array(),
112 'text/html' => array(),
113 );
114
115 return new TokenRow($this, $key);
116 }
117
118 /**
119 * @param array $params
120 * Array with keys:
121 * - entity: string, e.g. "profile".
122 * - field: string, e.g. "viewUrl".
123 * - label: string, e.g. "Default Profile URL (View Mode)".
4b350175 124 * @return TokenProcessor
8adcd073
TO
125 */
126 public function addToken($params) {
127 $key = $params['entity'] . '.' . $params['field'];
128 $this->tokens[$key] = $params;
129 return $this;
130 }
131
132 /**
133 * @param string $name
134 * @return array
135 * Keys:
136 * - string: Unprocessed message (eg "Hello, {display_name}.").
137 * - format: Media type (eg "text/plain").
138 */
139 public function getMessage($name) {
140 return $this->messages[$name];
141 }
142
143 /**
144 * Get a list of all tokens used in registered messages.
145 *
146 * @return array
147 */
148 public function getMessageTokens() {
149 $tokens = array();
150 foreach ($this->messages as $message) {
151 $tokens = \CRM_Utils_Array::crmArrayMerge($tokens, $message['tokens']);
152 }
153 foreach (array_keys($tokens) as $e) {
154 $tokens[$e] = array_unique($tokens[$e]);
155 sort($tokens[$e]);
156 }
157 return $tokens;
158 }
159
160 public function getRow($key) {
161 return new TokenRow($this, $key);
162 }
163
164 /**
165 * @return \Traversable<TokenRow>
166 */
167 public function getRows() {
168 return new TokenRowIterator($this, new \ArrayIterator($this->rowContexts));
169 }
170
171 /**
172 * Get the list of available tokens.
173 *
174 * @return array
175 * Ex: $tokens['event'] = array('location', 'start_date', 'end_date').
176 */
177 public function getTokens() {
178 if ($this->tokens === NULL) {
179 $this->tokens = array();
180 $event = new TokenRegisterEvent($this, array('entity' => 'undefined'));
181 $this->dispatcher->dispatch(Events::TOKEN_REGISTER, $event);
182 }
183 return $this->tokens;
184 }
185
186 /**
187 * Compute and store token values.
188 */
189 public function evaluate() {
190 $event = new TokenValueEvent($this);
191 $this->dispatcher->dispatch(Events::TOKEN_EVALUATE, $event);
192 return $this;
193 }
194
195 /**
196 * Render a message.
197 *
198 * @param string $name
199 * The name previously registered with addMessage().
200 * @param TokenRow|int $row
201 * The object or ID for the row previously registered with addRow().
202 * @return string
203 * Fully rendered message, with tokens merged.
204 */
205 public function render($name, $row) {
206 if (!is_object($row)) {
207 $row = $this->getRow($row);
208 }
209
210 $message = $this->getMessage($name);
211 $row->fill($message['format']);
212 $useSmarty = !empty($row->context['smarty']);
213
214 // FIXME preg_callback.
215 $tokens = $this->rowValues[$row->tokenRow][$message['format']];
216 $flatTokens = array();
217 \CRM_Utils_Array::flatten($tokens, $flatTokens, '', '.');
218 $filteredTokens = array();
219 foreach ($flatTokens as $k => $v) {
220 $filteredTokens['{' . $k . '}'] = ($useSmarty ? \CRM_Utils_Token::tokenEscapeSmarty($v) : $v);
221 }
222
223 $event = new TokenRenderEvent($this);
224 $event->message = $message;
225 $event->context = $row->context;
226 $event->row = $row;
227 $event->string = strtr($message['string'], $filteredTokens);
228 $this->dispatcher->dispatch(Events::TOKEN_RENDER, $event);
229 return $event->string;
230 }
231
232}
233
234class TokenRowIterator extends \IteratorIterator {
235
236 protected $tokenProcessor;
237
238 /**
239 * @param TokenProcessor $tokenProcessor
240 * @param Traversable $iterator
241 */
242 public function __construct(TokenProcessor $tokenProcessor, Traversable $iterator) {
243 parent::__construct($iterator); // TODO: Change the autogenerated stub
244 $this->tokenProcessor = $tokenProcessor;
245 }
246
247 public function current() {
248 return new TokenRow($this->tokenProcessor, parent::key());
249 }
250
251}