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
Contactwho registered; theEventfor which they registered; and theParticipantrecord 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; notFOOorFoo) - 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_textmsg_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_ids, 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
@scopeannotation 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
tokenContextand/ortplParamsdata, it can be imported into a class model. Recognized values are mapped via@scope, and unrecognized values are mapped to a generic$_extrasarray. - If you have a class and need
tokenContextortplParamsdata, it can be exported to arrays. As with import, recognized values are mapped via@scope, and$_extraswill 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 becontactIdoractivityId.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_htmlisTest(bool): Wrap the template in a test bannerdisableSmarty(bool): Force evaluation ofmessageTemplatein Token-Only language (overriding the default Token-Smarty language).(subjectstring, deprecated): Override the default subject. (messageTemplate.msg_subjectis 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)