← Voltar ao blog

Email templates customizados no Magento 2: declarar, enviar e sobrescrever

Rodrigo Biazzi

Cedo ou tarde todo projeto Magento precisa enviar um e-mail que o core não tem: a confirmação de um pedido de orçamento, um aviso de produto de volta ao estoque, uma notificação para o depósito. Fazer direito é mais do que chamar a função mail do PHP. O template deve ser traduzível, editável pelo lojista no admin, estilizado como os outros e-mails da loja e enviado com o remetente correto da store.

Este artigo cobre email templates customizados no Magento Open Source e no Adobe Commerce 2.4.8: declarar o template, escrevê-lo na sintaxe atual, enviá-lo com o TransportBuilder, deixar os admins sobrescreverem e sobrescrever templates do core em um tema.

Declarando o template

Os templates são registrados em etc/email_templates.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd">
    <template id="vendor_quote_email_confirmation"
              label="Quote Request Confirmation"
              file="quote_confirmation.html"
              type="html"
              module="Vendor_Quote"
              area="frontend"/>
</config>

Com area="frontend", o Magento procura o arquivo em view/frontend/email/quote_confirmation.html do módulo, ou no tema ativo se ele sobrescrever. O label é o que os admins veem ao carregar o template padrão.

O id não é arbitrário se você quer que os admins escolham um template na configuração. Volto a isso mais abaixo.

Escrevendo o template

<!--@subject {{trans "Your quote request at %store_name" store_name=$store.frontend_name}} @-->
<!--@vars {
"var customer_name":"Customer Name",
"var request_id":"Request ID",
"var store.frontend_name":"Store Name"
} @-->

{{template config_path="design/email/header_template"}}

<p class="greeting">{{trans "Hello %name," name=$customer_name}}</p>
<p>{{trans "We received your quote request #%id and will answer within one business day." id=$request_id}}</p>

{{if comment}}
<p>{{trans "Your message:"}}</p>
<p>{{var comment}}</p>
{{/if}}

<p>{{trans 'You can see your requests in <a href="%url">your account</a>.' url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p>

{{template config_path="design/email/footer_template"}}

Algumas notas sobre a sintaxe:

  • O comentário @subject vira a linha de assunto. O comentário @vars só alimenta o diálogo "Insert Variable" no admin. Ele não limita o que você pode passar.
  • {{trans}} torna o texto traduzível pelos dicionários CSV normais, com placeholders nomeados. Veja traduções e i18n.
  • Header e footer vêm da configuração de design, então o seu e-mail combina com os outros e-mails da loja e herda o logo e os estilos deles.
  • {{if}} e {{depend}} escondem blocos quando uma variável está vazia. {{for}} percorre arrays.
  • A saída de {{var}} é escapada. Use o modificador raw só para HTML que você mesmo montou e em que confia.

Strict mode

Desde a 2.3.4, os templates são processados em strict mode. Você pode ler dados de variáveis, inclusive getters de DataObject como $store.frontend_name, mas não pode chamar métodos arbitrários de objetos no template. Tutoriais antigos com {{var order.getSomething()}} não funcionam em templates novos. Helpers do próprio template filter, como a chamada $this.getUrl() no exemplo acima, são permitidos e usados do mesmo jeito nos templates do core. Templates salvos no banco antes da mudança podem ser marcados como legacy e continuar com o comportamento antigo até serem editados.

A regra prática: prepare os dados em PHP e passe valores simples ou arrays. Também é mais seguro, porque um template editado no admin não consegue executar código que não deveria.

Enviando o e-mail

<?php
declare(strict_types=1);

namespace Vendor\Quote\Model;

use Magento\Framework\App\Area;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Mail\Template\TransportBuilder;
use Magento\Framework\Translate\Inline\StateInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;

class ConfirmationSender
{
    private const XML_PATH_TEMPLATE = 'vendor_quote/email/confirmation';
    private const XML_PATH_IDENTITY = 'vendor_quote/email/identity';

    public function __construct(
        private readonly TransportBuilder $transportBuilder,
        private readonly StateInterface $inlineTranslation,
        private readonly ScopeConfigInterface $scopeConfig,
        private readonly StoreManagerInterface $storeManager
    ) {
    }

    public function send(string $email, string $name, string $requestId, int $storeId, string $comment = ''): void
    {
        $store = $this->storeManager->getStore($storeId);
        $templateId = (string) $this->scopeConfig->getValue(self::XML_PATH_TEMPLATE, ScopeInterface::SCOPE_STORE, $storeId);
        $identity = (string) $this->scopeConfig->getValue(self::XML_PATH_IDENTITY, ScopeInterface::SCOPE_STORE, $storeId);

        $this->inlineTranslation->suspend();
        try {
            $transport = $this->transportBuilder
                ->setTemplateIdentifier($templateId)
                ->setTemplateOptions(['area' => Area::AREA_FRONTEND, 'store' => $storeId])
                ->setTemplateVars([
                    'customer_name' => $name,
                    'request_id' => $requestId,
                    'comment' => $comment,
                    'store' => $store,
                ])
                ->setFromByScope($identity ?: 'general', $storeId)
                ->addTo($email, $name)
                ->getTransport();

            $transport->sendMessage();
        } finally {
            $this->inlineTranslation->resume();
        }
    }
}

Alguns detalhes que importam:

  • Store id nas template options. Ele decide o locale, o design e as URLs da store. Sem ele, um e-mail enviado pelo cron ou pelo admin pode sair no idioma errado e com URLs do admin.
  • Inline translation suspensa. Se a inline translation estiver ligada para debug, ela injetaria seu markup no e-mail.
  • Remetente por scope. O setFromByScope() recebe um código de identidade como general, sales ou support e resolve o nome e o e-mail a partir de Stores, Configuration, General, Store Email Addresses.
  • Erros. O sendMessage() lança Magento\Framework\Exception\MailException em caso de falha. Capture no chamador, registre no log e não deixe um problema do servidor de e-mail quebrar a request que disparou o envio.

Enviando muitos e-mails

Para uma confirmação única, enviar dentro da request está bom. Para um lote, como um lembrete para todo cliente com orçamento aberto, não faça loop sobre milhares de destinatários em um controller ou em uma única execução de cron. Publique uma mensagem por destinatário em uma queue e envie a partir de um consumer, para que um servidor de e-mail lento ou um endereço ruim não pare o lote inteiro e as mensagens que falharam possam ser reenviadas. O mesmo vale para qualquer coisa disparada por uma mass action no admin.

Note também que o TransportBuilder no 2.4 não tem método público para anexos. Se você precisa anexar um arquivo, como um orçamento em PDF, vai precisar de um builder próprio ou de um plugin que adicione uma parte MIME à mensagem. Veja como o seu mail transport ou a extensão de SMTP lida com isso antes de escolher.

Deixando os admins escolherem o template

Lojistas esperam editar e-mails em Marketing, Communications, Email Templates, carregar o seu padrão, alterar e depois selecionar a versão deles na configuração. Adicione um campo no system.xml:

<field id="confirmation" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
    <label>Confirmation Email Template</label>
    <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model>
</field>
<field id="identity" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
    <label>Email Sender</label>
    <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model>
</field>

E um valor padrão no config.xml:

<default>
    <vendor_quote>
        <email>
            <confirmation>vendor_quote_email_confirmation</confirmation>
            <identity>general</identity>
        </email>
    </vendor_quote>
</default>

Aqui vai o detalhe sobre o id. O source model do template monta sua lista a partir do config path do campo, trocando as barras por underscores. O path vendor_quote/email/confirmation vira vendor_quote_email_confirmation, e é por isso que o id do template no email_templates.xml usa exatamente esse nome. Se não baterem, o dropdown mostra só templates customizados e nenhum padrão.

Quando um admin seleciona um template do banco, o valor da config vira o id numérico dele, e o setTemplateIdentifier() o carrega do banco em vez do arquivo. O seu código de envio não muda.

Sobrescrevendo templates do core em um tema

Para e-mails do core, como a confirmação de pedido, você tem duas opções. Templates do admin são fáceis de editar para os lojistas, mas ficam só no banco e se perdem facilmente entre ambientes. Arquivos de tema ficam no git. Para sobrescrever no tema, copie o arquivo para a mesma pasta do módulo dentro do seu tema:

mkdir -p app/design/frontend/Vendor/theme/Magento_Sales/email
cp vendor/magento/module-sales/view/frontend/email/order_new.html \
   app/design/frontend/Vendor/theme/Magento_Sales/email/order_new.html

Mantenha os arquivos sobrescritos no mínimo e anote de qual versão do core você copiou. Depois de cada upgrade, compare com os novos arquivos do core, porque os templates do core às vezes mudam variáveis. O checklist de upgrade tem um passo para isso.

Os estilos dos e-mails vêm de email.less e email-inline.less na pasta web/css do tema. O template de header os carrega com as diretivas {{css}} e {{inlinecss}}, e os estilos inline são aplicados quando o e-mail é gerado, porque muitos clientes de e-mail ignoram tags style.

Testando e-mails

Nunca teste com envio real a partir de um ambiente de desenvolvimento. Rode um catcher como Mailpit ou MailHog e aponte o Magento para ele. Setups com Docker como o Warden já incluem um, como descrito em o guia do ambiente local com Docker. Teste cada store view, já que o locale e o design dependem do store id, e teste com um nome longo e com um campo opcional vazio.

Para e-mails de vendas, confira também a configuração "Asynchronous sending" em Stores, Configuration, Sales, Sales Emails, General Settings. Com ela ligada, os e-mails de pedido são enviados pelo cron em vez de durante a request, o que é mais rápido para o cliente mas significa que sem cron não há e-mail.

FAQ

Por que meu email template customizado não aparece no dropdown da configuração?

O id do template no email_templates.xml precisa bater com o config path do campo, com as barras trocadas por underscores. Confira também se o módulo está habilitado e se o cache está limpo.

Por que as variáveis do meu template estão vazias?

O strict mode não permite chamadas arbitrárias de método nos templates. Prepare os valores em PHP e passe em setTemplateVars, depois leia com var.

Devo editar os templates de e-mail do core no admin ou no tema?

Arquivos de tema são versionados e vão iguais para todos os ambientes. Templates do admin são mais fáceis para os lojistas. Muitos times usam o tema para a estrutura e deixam os lojistas mudarem o texto no admin, mas escolha um dono por template.

Por que o e-mail sai no idioma errado?

O store id está ausente ou errado no setTemplateOptions. Passe a store do cliente ou do pedido, e não a store atual, ao enviar pelo cron ou pelo admin.

Atendo como freelancer em Magento e Adobe Commerce, incluindo mudanças em e-mails transacionais. Se você precisa de ajuda com isso, fale comigo.

Artigos relacionados