Shipping Templates with a Module
Ship a notification with your module, so enabling it is all the operator has to do.
Overview
A module that sends its own mail installs two things. The registration puts the template on the Notification Templates screen, where the operator turns it on or off. The files put the subject, the body and the text message where they are read from.
Skip the first and dispatch() returns disabled. Skip the second and the mail goes out with no body and no subject. One call does both.
Prerequisites
- A module with an
enable()path. - A group name. Reuse a core group when the mail belongs to that domain, or use your own.
- Read How Notification Templates Work first. It explains the roots this call writes into.
Structure
Keep the templates inside the module, so they travel and are removed with it. Two source shapes are read; use whichever suits you.
coremio/modules/Servers/Acme/
├── Acme.php
└── notifications/ # flat form: one file per part
├── en/
│ ├── acme-quota-reached.json # {"subject": "..."}
│ ├── acme-quota-reached.html # the mail body
│ └── acme-quota-reached.txt # the text message
└── tr/ ...
coremio/modules/Servers/Acme/notifications/ # folder form: adds a per-design body
└── en/acme-quota-reached/
├── content.json
├── content.txt
├── content.html # the base design's body
└── ledger.html # the Ledger design's own body
The folder form is what a release package uses, so a module and a release describe a template alike.
Walkthrough
- Write the mail body as a fragment. The shell around it comes from the active design.
- Write one file per language you support. A language you skip has no message; the operator can fill it in.
- Use Smarty placeholders (
{$service_name}) and declare them insettings, so the panel offers them. - Call the seeder from
enable(). Add a guard that also runs on update: an installation enabled long ago never toggles the module to receive a new template. - Send with
dispatch()and read the returnedstatus.disabledmeans the registration is missing or the operator turned it off.
Reference
static function seed_templates(string $group, array $templates, array $options = []): array
{
// ...
}
status 1, user-mail 1, admin-mail/user-sms/admin-sms 0, empty emails/phones/departments. Leave the key out entirely and the config file is not touched at all — for a module that maintains its own entry.
source: [lang => ['subject' => …, 'html' => …, 'sms' => …, 'themes' => [design => html]]]. A part you leave out is not written.
'base' (default) writes the body to the design that ships with the product and lets every other design fall back to it. 'all' copies that body into each installed design — see the pitfall below before choosing it.
['written' => string[], 'skipped' => int, 'registered' => string[]] — paths written this run, files left alone because they already existed, and the group/key pairs added to the config.
Example
private function ensure_notification_template(): void
{
static $checked = false;
if ($checked) return;
$checked = true;
\Notification::seed_templates('service', [
'acme-quota-reached' => [
'settings' => [
'variables' => '{service_id},{service_name},{quota_usage}',
'status' => 1,
'user-mail' => 1,
],
'source' => __DIR__ . DS . 'notifications',
],
]);
}
$this->ensure_notification_template();
$result = \Notification::dispatch('service', 'acme-quota-reached', [
'user_id' => (int) $service['owner_id'],
'variables' => [
'{service_id}' => $service['id'],
'{service_name}' => $service['name'],
'{quota_usage}' => $usage . '%',
],
]);
// 'disabled' is a decision, not a failure: the operator turned this mail off.
if (($result['status'] ?? '') === 'error') \Logger::error('Acme quota mail failed');
Pitfalls
A design with no file for the event shows the base body inside its own shell. Copying the body into every design is not the same thing. The copy outranks the fallback, so that theme's author can never ship a design for the mail afterwards.
A config entry with no files sends an empty mail. The first sign of it is a customer complaint. If you maintain the entry yourself, still call the seeder for the files. Pass no settings key and your entry is left alone.
The parts live in different roots, and the body's root depends on the active design. Code that joins templates/notifications/ to a language folder writes where nothing reads. The write succeeds and the mail stays empty.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.