Skip to main content

Email Type Provider Interface

App plugins can register their own email types with J2Commerce's core email template system. Once registered, the email type appears in the Email Templates admin UI, its shortcodes appear in the template editor, and emails are sent via EmailHelper::getTemplateByType() with full template customisation by the store owner.

The app_reviews plugin (version 6.1.0) is the canonical implementation. The patterns shown here come directly from that plugin.

Architecture

Interface

// File: administrator/components/com_j2commerce/src/Contract/EmailTypeProviderInterface.php

namespace J2Commerce\Component\J2commerce\Administrator\Contract;

interface EmailTypeProviderInterface
{
/**
* Return all email types provided by this plugin.
* Keyed by type identifier (e.g. 'reviews').
*/
public function getEmailTypes(): array;

/**
* Return true if this plugin handles the given email type.
*/
public function handlesEmailType(string $emailType): bool;

/**
* Return tag name => replacement value pairs for a specific send context.
* Called by EmailHelper at send time.
*/
public function getEmailTagValues(string $emailType, string $context, object $data): array;
}

Minimum Viable Implementation

1. Declare the interface and subscribe to events

// File: plugins/j2commerce/app_example/src/Extension/AppExample.php

declare(strict_types=1);

namespace MyVendor\Plugin\J2commerce\AppExample\Extension;

use J2Commerce\Component\J2commerce\Administrator\Contract\EmailTypeProviderInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;

final class AppExample extends CMSPlugin implements SubscriberInterface, EmailTypeProviderInterface
{
public static function getSubscribedEvents(): array
{
return [
'onJ2CommerceRegisterEmailTypes' => 'onRegisterEmailTypes',
'onJ2CommerceGetEmailTemplateCards' => 'onGetEmailTemplateCards',
'onJ2CommerceGetEmailTemplates' => 'onGetEmailTemplates',
];
}

2. Register the email type

    public function onRegisterEmailTypes(Event $event): void
{
$registry = $event->getArgument('registry');

$registry->registerType('example', [
'type' => 'example',
'label' => 'PLG_J2COMMERCE_APP_EXAMPLE_EMAILTYPE_LABEL',
'description' => 'PLG_J2COMMERCE_APP_EXAMPLE_EMAILTYPE_DESC',
'icon' => 'fa-solid fa-envelope',
'contexts' => [
'example_notification' => 'PLG_J2COMMERCE_APP_EXAMPLE_CONTEXT_NOTIFICATION',
],
'tags' => $this->getEmailTags(),
'default_subject' => 'PLG_J2COMMERCE_APP_EXAMPLE_DEFAULT_SUBJECT',
'default_body' => 'PLG_J2COMMERCE_APP_EXAMPLE_DEFAULT_BODY',
'receiver_types' => ['customer'],
]);
}

The tags key is an array of tag definitions used to populate the shortcode picker in the editor. See Tag definition format below.

3. Implement getEmailTypes and handlesEmailType

These satisfy the EmailTypeProviderInterface contract and must match the type string registered in onRegisterEmailTypes:

    public function getEmailTypes(): array
{
return [
'example' => [
'type' => 'example',
'label' => 'PLG_J2COMMERCE_APP_EXAMPLE_EMAILTYPE_LABEL',
'receiver_types' => ['customer'],
],
];
}

public function handlesEmailType(string $emailType): bool
{
return $emailType === 'example';
}

4. Supply tag values at send time

This is where shortcodes become actual values. $data is the context object your plugin passes when calling the mailer:

    public function getEmailTagValues(string $emailType, string $context, object $data): array
{
return [
'CUSTOMER_NAME' => htmlspecialchars($data->user->name ?? '', ENT_QUOTES, 'UTF-8'),
'PRODUCT_NAME' => htmlspecialchars($data->product->title ?? '', ENT_QUOTES, 'UTF-8'),
'STORE_NAME' => htmlspecialchars($data->storeName ?? '', ENT_QUOTES, 'UTF-8'),
];
}

The keys here must match the tag names registered in getEmailTags(). The square-bracket wrapping ([CUSTOMER_NAME] → value) is handled by EmailHelper::getTemplateByType().

5. Send using EmailHelper

    protected function sendExampleEmail(object $request): bool
{
$data = $this->buildEmailData($request);
$tagValues = $this->getEmailTagValues('example', 'example_notification', $data);

$template = \J2Commerce\Component\J2commerce\Administrator\Helper\EmailHelper::getTemplateByType(
'example',
'example_notification'
);

if ($template !== null) {
$subject = $template->subject;
$body = $template->body;

foreach ($tagValues as $tag => $value) {
$subject = str_replace('[' . $tag . ']', $value, $subject);
$body = str_replace('[' . $tag . ']', $value, $body);
}
} else {
// Fallback to plain language string
$subject = \Joomla\CMS\Language\Text::_('PLG_J2COMMERCE_APP_EXAMPLE_DEFAULT_SUBJECT');
$body = \Joomla\CMS\Language\Text::_('PLG_J2COMMERCE_APP_EXAMPLE_DEFAULT_BODY');
}

$mailerFactory = \Joomla\CMS\Factory::getContainer()
->get(\Joomla\CMS\Mail\MailerFactoryInterface::class);
$mailer = $mailerFactory->createMailer();
$config = $this->getApplication()->getConfig();

$mailer->setSender([$config->get('mailfrom'), $config->get('fromname')]);
$mailer->addRecipient($data->user->email, $data->user->name);
$mailer->setSubject($subject);
$mailer->setBody($body);
$mailer->isHtml(true);

return $mailer->Send() === true;
}

Tag Definition Format

Each entry in the getEmailTags() array describes one shortcode:

private function getEmailTags(): array
{
return [
'CUSTOMER_NAME' => [
'label' => 'PLG_J2COMMERCE_APP_EXAMPLE_TAG_CUSTOMER_NAME',
'description' => 'PLG_J2COMMERCE_APP_EXAMPLE_TAG_CUSTOMER_NAME_DESC',
'group' => 'customer', // Groups tags in the picker sidebar
],
'PRODUCT_NAME' => [
'label' => 'PLG_J2COMMERCE_APP_EXAMPLE_TAG_PRODUCT_NAME',
'description' => 'PLG_J2COMMERCE_APP_EXAMPLE_TAG_PRODUCT_NAME_DESC',
'group' => 'product',
],
];
}

The group value controls which collapsible section the tag appears in within the template editor's shortcode panel. Common group names used by J2Commerce: customer, product, order, store, review.

Shipping a Bundled HTML Template

Plugins can ship pre-designed HTML files that store owners can load into the editor from the template picker.

Step 1: Create the HTML file

Place template HTML under the plugin's tmpl/email/ directory:

plugins/j2commerce/app_example/tmpl/email/default.html

Use shortcodes directly in the HTML as [CUSTOMER_NAME], [PRODUCT_NAME], etc.

Step 2: Register a template card

The card appears in the template picker modal when creating or editing an email template:

    public function onGetEmailTemplateCards(Event $event): void
{
$cards = $event->getArgument('result', []);

$cards[] = [
'type' => 'example',
'email_type' => 'example',
'context' => 'example_notification',
'design' => 'default',
'label' => 'PLG_J2COMMERCE_APP_EXAMPLE_TEMPLATE_DEFAULT',
'description' => 'PLG_J2COMMERCE_APP_EXAMPLE_TEMPLATE_DEFAULT_DESC',
'thumbnail' => 'media/plg_j2commerce_app_example/images/template_default_thumb.webp',
'template_file' => JPATH_PLUGINS . '/j2commerce/app_example/tmpl/email/default.html',
];

$event->setArgument('result', $cards);
}

Step 3: Serve the HTML when the admin loads the card

    public function onGetEmailTemplates(Event $event): void
{
$type = $event->getArgument('type', '');
$design = $event->getArgument('design', '');

if ($type !== 'example') {
return;
}

$allowed = ['default'];

if (!\in_array($design, $allowed, true)) {
return;
}

$filePath = JPATH_PLUGINS . '/j2commerce/app_example/tmpl/email/' . $design . '.html';

if (!is_file($filePath)) {
return;
}

$result = $event->getArgument('result', ['body' => null]);
$result['body'] = file_get_contents($filePath);
$event->setArgument('result', $result);
}

The plg: prefix in body_source_file

When a default email template row is inserted into #__j2commerce_emailtemplates with body_source = 'file', the body_source_file column can use a plugin-prefixed path to avoid copying the HTML into the component directory:

plg:j2commerce.app_example:default.html

EmailHelper::getTemplateFromFile() resolves this as:

JPATH_PLUGINS/j2commerce/app_example/tmpl/email/default.html

The format is plg:[group].[name]:[relative/path.html]. Without the prefix, the path is resolved under administrator/components/com_j2commerce/layouts/templates/email/.

Insert the default template row in your install SQL:

INSERT IGNORE INTO `#__j2commerce_emailtemplates`
(`email_type`, `context`, `receiver_type`, `orderstatus_id`, `group_id`,
`paymentmethod`, `subject`, `body`, `body_source`, `body_source_file`,
`language`, `enabled`, `ordering`)
VALUES
('example', 'example_notification', 'customer', '*', '*', '*',
'Default subject line',
'', 'file', 'plg:j2commerce.app_example:default.html',
'*', 1, 1);

Using INSERT IGNORE makes the statement idempotent — safe to re-run on update without duplicating templates.

Event Contract Summary

EventWhen firedYour handler must
onJ2CommerceRegisterEmailTypesAdmin loads Email TemplatesCall $registry->registerType() with type config
onJ2CommerceGetEmailTemplateCardsAdmin opens template pickerAppend card array(s) to $event->getArgument('result', []), then call $event->setArgument('result', $cards)
onJ2CommerceGetEmailTemplatesAdmin clicks "Load Template"Set $result['body'] to the HTML string, call $event->setArgument('result', $result)

Important: SubscriberInterface handlers must not return values — the dispatcher discards them. Always use $event->setArgument('result', ...) to pass data back.

The Dead-Shortcode Pitfall

Registering tags in onRegisterEmailTypes makes them appear in the editor sidebar, but does not make them work in sent emails. Tags are replaced only if getEmailTagValues() returns a matching key at send time.

The common mistake:

// Tags visible in editor — but getEmailTagValues() only returns 3 of 20 keys
// Result: 17 shortcodes render as literal "[TAG_NAME]" in the customer's inbox

Before version 6.1.0, app_reviews had exactly this bug: 20 shortcodes were shown to the admin but only 5 were replaced in the sent email. The fix requires that every tag name in getEmailTags() has a matching key in getEmailTagValues().

Check all 20 tags are covered:

// Validate at development time — not in production code
$tags = array_keys($this->getEmailTags());
$values = array_keys($this->getEmailTagValues('example', 'example_notification', $dummyData));
$missing = array_diff($tags, $values);
// $missing must be empty

Queue Integration

If your plugin sends emails on a delayed schedule, use QueueHelper to avoid blocking the request:

use J2Commerce\Component\J2commerce\Administrator\Helper\QueueHelper;

// Enqueue — returns the new queue_id
$queueId = QueueHelper::enqueue(
'app_example', // queue_type (your plugin's unique identifier)
(string) $requestId, // relation_id (FK to your domain table)
['request_id' => $requestId, 'product_id' => $productId],
'example_notification', // item_type
0, // priority (0 = normal)
5 // max_attempts
);

Subscribe to onQueueProcess and check $item->queue_type:

public function onQueueProcess(Event $event): void
{
$item = $event->getArgument('item', null);

if (!$item || ($item->queue_type ?? '') !== 'app_example') {
return;
}

$data = json_decode($item->queue_data, true);
// ... load domain data, call sendExampleEmail(), then:

if ($success) {
QueueHelper::complete((int) $item->j2commerce_queue_id);
} else {
QueueHelper::fail((int) $item->j2commerce_queue_id, 'Email send failed');
}
}

QueueHelper::fail() applies exponential backoff (2^n minutes, capped at 24 hours) and marks the item as dead when max_attempts is exceeded. Failed items are visible in the J2Commerce admin queue view at J2Commerce -> Configuration -> Queues.