Workflow Messages¶
Overview¶
It is common to send an automated email-message whenever a constituent registers for an event, makes a pledge for a future donation, or performs a similar action.
The code is likely to look something like:
$renderedMessage = WorkflowMessage::render()
->setWorkflow('contribution_online_receipt')
->setLanguage('de_DE')
->setValues(['contributionID' => $this->getContributionID(), 'contactID' => $this->getContactID()])
->execute()
->first();
Each automated message represents a step in a workflow, and each automated message has a few key ingredients.
For example:
- Use Case: When a constituent registers for an event, send an email receipt.
- Message Name:
event_receipt
- Data (Model): The
Contact
who registered; theEvent
for which they registered; and theParticipant
record with details about their specific registration.[ 'contactId' => 100, 'eventId' => 200, 'participantId' => 300, ]
- Template (View): The HTML or prose which thanks them for signing up, highlights key details of the
event (e.g. start-time and location), and perhaps offers follow-up links (e.g. to cancel the registration or
fill-out a pre-event survey).
<p>Dear {contact.first_name},</p> <p>Thank you for registering for {event.title}! See you on {event.start_date}!</p>
This example involves a contract: whenever a PHP application fires event_receipt
, it must supply eventId
so that tokens like {event.start_date}
can be evaluated. Similarly, if the administrator edits the template, they may safely expect {event.*}
(eventId
) to be available, but they should not
expect {case.*}
(caseId
). The contract is important for the administrative screens (eg providing a list of relevant tokens and examples).
This chapter examines how to register workflow message templates, define their contracts, and render them.
Registration¶
Suppose we are building CiviCheese, the cheese-making management system for non-governmental organizations. We might send automated messages after completing each step of the cheese-making process (e.g. curdling, aging, cutting). Each automated message should be registered before usage.
Workflow names¶
The message will be composed as a step in some workflow, and we must choose a machine name for this step.
If we were adding a new subsystem like CiviCheese with several distinct messages, then we would choose a name for each message
(eg cheese_curdling
, cheese_aging
, cheese_cutting
).
Naming convention
- Use lower-case alphanumerics (eg
foo
; notFOO
orFoo
) - Separate words with underscores (eg
foo_bar
; notFOO-BAR
; notfoo bar
; notFooBar
) - Prefix with a subsystem or entity name (eg
contribution_*
,case_*
)
How To: Show a list of existing names (canonically)
The WorkflowMessage
API can be used to browse and inspect the registered messages.
cv api4 WorkflowMessage.get -T +s name
+----------------------------------+
| name |
+----------------------------------+
| generic |
| contribution_invoice_receipt |
| contribution_offline_receipt |
| contribution_online_receipt |
| contribution_recurring_cancelled |
| contribution_recurring_edit |
| participant_cancelled |
| participant_transferred |
...
How To: Show a list of existing names (exhaustively)
Some messages may be informally or incompletely registered. These do not appear in WorkflowMessage
API.
The MessageTemplate
API provides another angle for listing messages. It should provide a mix of formally and informally defined messages.
cv api4 MessageTemplate.get -T '{"select":["workflow_name"], "groupBy":["workflow_name"]}'
+----------------------------------+
| workflow_name |
+----------------------------------+
| case_activity |
| contribution_dupalert |
| contribution_offline_receipt |
| contribution_online_receipt |
| contribution_invoice_receipt |
| contribution_recurring_notify |
...
Templates¶
During installation or setup, we should register the message-templates.
Each message-template includes typical email content (msg_subject
, , and msg_text
msg_html
). Note that including msg_text is deprecated and we have been updating these to be an empty string.
Email content allows a mix of CiviCRM token-notation and Smarty notation. Both of these examples are valid content:
<!-- CiviCRM-style tokens -->
<p>Hello {contact.first_name}!</p>
<!-- Smarty-style conditions, variables, blocks, functions -->
{if $cheeseBatchId > 10}<p>This is gonna be good.</p>{/if}
Technically, each workflow step (e.g. cheese_curdling
) requires two template records -- the default template and the reserved template.
These templates are nearly identical.
What is the difference between the default template and the reserved template?
At runtime, when composing a message, the system loads and evaluates the default template. Administrators may customize the default template.
The reserved template provides a reference-point -- if a customization goes awry, the administrator can refer back to the reserved template and examine differences.
How To: Show a list of existing message-templates
The MessageTemplate
API can be used to browse, inspect, and update templates.
cv api4 MessageTemplate.get -T +s id,workflow_name,is_default,is_reserved
+----+----------------------------------+------------+-------------+
| id | workflow_name | is_default | is_reserved |
+----+----------------------------------+------------+-------------+
| 1 | case_activity | 1 | |
| 2 | case_activity | | 1 |
| 3 | contribution_dupalert | 1 | |
| 4 | contribution_dupalert | | 1 |
| 5 | contribution_offline_receipt | 1 | |
| 6 | contribution_offline_receipt | | 1 |
...
Example: Register message-templates for "cheese_curdling"
$baseTpl = [
'workflow_name' => 'cheese_curdling',
'msg_title' => 'Cheese - Curdling finished',
'msg_subject' => 'Curdling completed [#{$cheeseBatchId}]'),
'msg_text' => 'Hey, {contact.first_name}! Cheese batch #{$cheeseBatchId} ({$cheeseType}) has finished curdling!',
'msg_html' => '<p>Hey, {contact.first_name}! Cheese batch #{$cheeseBatchId} ({$cheeseType}) has finished curdling!</p>',
];
// Create a "reserved" template. This is a pristine copy provided for reference.
civicrm_api4('MessageTemplate', 'create',
['values' => $baseTpl + ['is_reserved' => 1, 'is_default' => 0],
]);
// Create a default template. This is live. The administrator may edit/customize.
civicrm_api4('MessageTemplate', 'create',
['values' => $baseTpl + ['is_reserved' => 0, 'is_default' => 1],
]);
Do message-templates require workflow_id
s, option-groups, or option-values?
Historically, yes. Currently, no.
Historically, civicrm_msg_template
.workflow_id
referenced a civicrm_option_value
(and this record often referenced a
custom civicrm_option_group
). Some message-templates still define these for backward compatibility.
Currently, CiviCRM does not use or require workflow_id
or the related option-values. Instead, it uses workflow_name
.
Data model¶
Templates reference data, like in the token {contact.first_name}
or the Smarty variable {$cheeseBatchId}
. The
data model is the list of expected data, which may include:
- Token-processing data (
tokenContext
). For example, setting[contactId=>123]
will enable{contact.*}
tokens. - Smarty-template data (
tplParams
). For example, setting[cheeseType=>'cottage']
will enable{$cheeseType}
.
Prior to CiviCRM v5.43
, the data model was entirely ad hoc. There was no standard way to enumerate or document
data. CiviCRM v5.43
introduced an optional class-based data-model, which allows richer user-experience (better
token-lists, autocompletes, validation, previews, etc). Here's an example class:
Example: Define a class model for cheese_curdling
class CRM_Cheese_WorkflowMessage_CheeseCurdling extends GenericWorkflowMessage {
public const WORKFLOW = 'cheese_curdling';
/**
* @var string
* @options brie,cheddar,cottage,gouda
* @scope tplParams
*/
protected $cheeseType
/**
* @var int
* @scope tplParams
*/
protected $cheeseBatchId;
}
The example shows a few important points:
- The class name follows a convention (
CRM_{$component}_WorkflowMessage_{$workflow}
). - The base class is
\Civi\WorkflowMessage\GenericWorkflowMessage
. - Every input is a property of the class.
- The
@scope
annotation shares data with the token-processor (@scope tokenContext
) or Smarty template-engine (@scope tplParams
).
Question: Are adhoc models and class models interoperable?
Yes. This relies on bi-directional import/export mechanism.
- If you have an array of
tokenContext
and/ortplParams
data, it can be imported into a class model. Recognized values are mapped via@scope
, and unrecognized values are mapped to a generic$_extras
array. - If you have a class and need
tokenContext
ortplParams
data, it can be exported to arrays. As with import, recognized values are mapped via@scope
, and$_extras
will be read for any unrecognized values.
The class supports several more techniques -- such as default values, getters, setters, and so on. For more thorough discussion, see Class modeling.
Usage¶
As a developer, you choose between two primary actions, rendering or sending a workflow-message. Both actions may be executed through the BAO methods for internal CiviCRM code (they may change as they are primarily internal methods):
// Render the template(s) and return the resulting strings.
$rendered = \CRM_Core_BAO_MessageTemplate::renderTemplate([
'...data model options...',
'...template view options...'
]);
// Render the template(s) and send the resulting email.
$delivery = \CRM_Core_BAO_MessageTemplate::sendTemplate([
'...data model options...',
'...template view options...',
'...envelope options...'
]);
Equivalently, workflow-messages may be prepared in object-oriented fashion:
// Render the template(s) and return the resulting strings.
$model = new ExampleWorkflowMessage(['...data model options...']);
$rendered = $model->renderTemplate(['...template view options...']);
// Render the template(s) and send the resulting email.
$model = new ExampleWorkflowMessage(['...data model options...']);
$delivery = $model->sendTemplate([
'...template view options...',
'...envelope options...'
]);
All these methods are very similar - they evaluate a template, and they accept many of the same parameters. Parameters target a few different aspects of the message - the data (model), the template (view), and/or the email envelope.
Data model options¶
The data model options define the dynamic data that will be plugged into the message.
Example: Adhoc data model
With an adhoc data model, you specify a list of fields to pass through directly to the
templating system (e.g. tplParams
and tokenContext
). There is no fixed or formal
list of fields. This style works with existing callers (which predate the class-model).
$rendered = \CRM_Core_BAO_MessageTemplate::renderTemplate([
'workflow' => 'cheese_curdling',
'tokenContext' => ['contactId' => 123],
'tplParams' => ['cheeseType' => 'cottage', 'cheeseBatchId' => 456],
]);
Note: Internally, renderTemplate()
will recognize that cheese_curdling
corresponds to class
CRM_Cheese_WorkflowMessage_CheeseCurdling
. It will pass the tokenContext
and/or tplParams
into
the CheeseCurdling
class and apply any defaults, filters, validations, etc.
Example: Class data model
The class data model provides way to populate the data using getters, setters, and other
class-specific methods. Properties of the class may be inspected via ReflectionClass
or
via $model->getFields()
.
$model = (new CRM_Cheese_WorkflowMessage_CheeseCurdling())
->setContactId(123)
->setCheeseType('cottage')
->setCheeseBatchId(456);
$rendered = $model->renderTemplate();
As a message sender, you are only responsible for setting documented fields (in accordance with the
CheeseCurdling
contract). You do not know whether these fields are used for tokenContext
, tplParams
, or
something else.
Example: Hybrid - Class data model w/import from adhoc arrays
Sometimes, you may have code already written for an adhoc data model - but you wish to progressively convert it to the class model.
/** @var CRM_Cheese_WorkflowMessage_CheeseCurdling $model */
$model = WorkflowMessage::create('cheese_curdling', [
'tokenContext' => ['contactId' => 123],
'tplParams' => ['cheeseType' => 'cottage', 'cheeseBatchId' => 456],
]);
// ... Perform extra work, such as $model->validate(), then ...
$rendered = \CRM_Core_BAO_MessageTemplate::renderTemplate([
'model' => $model,
]);
WorkflowMessage::create()
accepts data from the adhoc format (tplParams
, tokenContext
), and it returns a
$model
object. This allows you to import existing data and then leverage any getters, setters, validators, etc.
The best available class will be matched by the name (eg cheese_curdling
=> CheeseCurdling
). If there is
no specific class, it will use GenericWorkflowMessage
.
The adhoc tplParams
and tokenContext
sometimes include unrecognized fields that are missing from the class
model. Unrecognized fields will be privately preserved in $model->_extras
. The model cannot directly manipulate
these fields, but they will be exported to the appropriate layer when rendering the template.
Reference: List of data model options
The data may be provided as a singular option:
model
(WorkflowMessageInterface
): The data model, defined as a class. The class may be exported toworkflow
,tplParams
,tokenContext
, etc.
Alternatively, the data may be an adhoc mix of:
workflow
(string
): Symbolic name of the workflow step. (For old-style workflow steps, this matches the option-value-name, a.k.a.valueName
.)modelProps
(array
): Define the list of model properties as an array. This is the same data that you would provide tomodel
, except in array notation instead of object notation.tplParams
(array
): This data is passed to the Smarty template evaluator.tokenContext
(array
): This data is passed to the token processing layer. Typical values might becontactId
oractivityId
.contactId
(int
): Alias fortokenContext.contactId
Template view options¶
The template view options define the layout, formatting, and prose of the message. If no view is specified,
renderTemplate()
and sendTemplate()
will autoload the default template. However, you may
substitute alternative template options.
Example: Load a default template
If you omit any view options (messageTemplateID
, messageTemplate
), then it will autoload the default template.
$rendered = \CRM_Core_BAO_MessageTemplate::renderTemplate([
// Data model options
'model' => $model,
]);
Example: Load a template by ID
$rendered = \CRM_Core_BAO_MessageTemplate::renderTemplate([
// Data model options
'model' => $model,
// Template view options
'messageTemplateID' => 123,
]);
Example: Pass in template content
$rendered = \CRM_Core_BAO_MessageTemplate::renderTemplate([
// Data model options
'model' => $model,
// Template view options
'messageTemplate' => [
'msg_subject' => 'Hello {contact.first_name}',
'msg_text' => 'The cheese is getting ready! {if $cheeseType=="cottage"}Time to prepare the fruit bowl!{/if}',
'msg_html' => '<p>The cheese is getting ready! {if $cheeseType=="cottage"}<strong>Time to prepare the fruit bowl!</strong>{/if}</p>',
],
]);
Reference: List of template view options
messageTemplateID
(int
): Load the template by its IDmessageTemplate
(array
): Use a template record that we have defined, overriding any autoloaded content. Keys:msg_subject
,msg_text
,msg_html
isTest
(bool
): Wrap the template in a test bannerdisableSmarty
(bool
): Force evaluation ofmessageTemplate
in Token-Only language (overriding the default Token-Smarty language).(subject
string
, deprecated): Override the default subject. (messageTemplate.msg_subject
is preferred.)
Note: If neither messageTemplate
nor messageTemplateID
is provided, then the default template-view will be determined by workflow
.
Envelope options¶
The envelope options describe any headers and addenda for the email message.
Reference: List of envelope options
from
(string
): The From: headertoName
(string
): The recipient’s nametoEmail
(string
): The recipient’s email - mail is sent only if setcc
(?
): The Cc: headerbcc
(?
): The Bcc: headerreplyTo
(?
): The Reply-To: headerattachments
(?
): Email attachmentsPDFFilename
(string
): Filename of optional PDF version to add as attachment (do not include path)