Home > Developer Guide > Framework
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.php [ '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).
```html
Dear {contact.first_name},
Thank you for registering for {event.title}! See you on {event.start_date}!
```
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
).
???+ tip "Naming convention"
* Use lower-case alphanumerics (eg `foo`; not `FOO` or `Foo`)
* Separate words with underscores (eg `foo_bar`; not `FOO-BAR`; not `foo bar`; not `FooBar`)
* Prefix with a subsystem or entity name (eg `contribution_*`, `case_*`)
??? example "How To: Show a list of existing names (canonically)"
The `WorkflowMessage` API can be used to browse and inspect the registered messages.
```bash
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 |
...
```
??? example "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
, ~~msg_text
~~, and 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.
??? question "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.
??? example "How To: Show a list of existing message-templates"
The `MessageTemplate` API can be used to browse, inspect, and update templates.
```bash
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 "Example: Register message-templates for "cheese_curdling""
```php
$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],
]);
```
??? question "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 "Example: Define a class model for cheese_curdling
"
```php
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 "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/or `tplParams` 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` or `tplParams` 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 "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).
```php
$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 "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()`.
```php
$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 "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.
```php
/** @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.
??? note "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 to `workflow`, `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 to `model`, 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](token.md). Typical values might be `contactId` or `activityId`.
* `contactId` (`int`): Alias for `tokenContext.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 "Example: Load a default template"
If you omit any view options (`messageTemplateID`, `messageTemplate`), then it will autoload the default template.
```php
$rendered = \CRM_Core_BAO_MessageTemplate::renderTemplate([
// Data model options
'model' => $model,
]);
```
??? example "Example: Load a template by ID"
```php
$rendered = \CRM_Core_BAO_MessageTemplate::renderTemplate([
// Data model options
'model' => $model,
// Template view options
'messageTemplateID' => 123,
]);
```
??? example "Example: Pass in template content"
```php
$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>',
],
]);
```
??? note "Reference: List of template view options"
* `messageTemplateID` (`int`): Load the template by its ID
* `messageTemplate` (`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 banner
* `disableSmarty` (`bool`): Force evaluation of `messageTemplate` 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.
??? note "Reference: List of envelope options"
* `from` (`string`): The From: header
* `toName` (`string`): The recipient’s name
* `toEmail` (`string`): The recipient’s email - mail is sent only if set
* `cc` (`?`): The Cc: header
* `bcc` (`?`): The Bcc: header
* `replyTo` (`?`): The Reply-To: header
* `attachments` (`?`): Email attachments
* `PDFFilename` (`string`): Filename of optional PDF version to add as attachment (do not include path)