How to Attach a PDF File to Email in Magento 2

As you might notice, some changes were made in Magento 2.3.3 core files related to emails. Unfortunately, the old ways of attaching files aren’t compatible anymore. I won’t go deep about what is the difference between versions. In this article I would like to give you a way to attach any file to any Magento 2 Email template. It has already been tested on Magento 2.3.3 and Magento 2.3.4 versions.

It’s going to be a technical article. Those who are looking for an easy way to control email attachments via Magento 2 admin side, can jump straight on “How to Attach a File via Magento 2 admin” paragraph.

Attaching a file to email programmatically

Here are the reasons why you should attach files to email using this way:

  • It’s compatible with Magento 2.3.3 or later versions
  • It is based on Magento 2 plugins
  • It isn’t overwriting any core class. You will avoid the conflicts with future Magento 2 updates.
  • It lets you attach any file to any Magento 2 transactional email.
  • It lets you attach multiple files.

I divided it into four steps:

  1. Create a basic module
  2. Register template id and template variables
  3. Add attachment to email
  4. Create an email attachment

1. Create a basic extension

Even if you already have an extension to work on, I suggest creating a new one according to this tutorial. It helps you avoid refactoring mistakes. I called the extension Magetrend_Attahment and will use this name in all following examples. To create a Magento 2 extension base we need to create two files: module.xml and registration.php

app/code/Magetrend/Attachment/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Magetrend_Attachment',
    __DIR__
);
app/code/Magetrend/Attachment/etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
    <module name="Magetrend_Attachment" schema_version="2.0.0" setup_version="2.0.0">
    </module>
</config>

2. Register template id and template variables.

If we want to attach a file to any Magento 2 template, we need to have template id and template variables, which could let us identify the current template. We can do that by creating a plugin for class:

Magento\Framework\Mail\Template\TransportBuilder
app/code/Magetrend/Attachment/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\Mail\Template\TransportBuilder">
        <plugin name="magetrend-attachment-transport-builder" type="Magetrend\Attachment\Plugin\TransportBuilder"/>
    </type>
</config>
app/code/Magetrend/Attachment/Plugin/TransportBuilder.php
<?php

namespace Magetrend\Attachment\Plugin;

class TransportBuilder
{
    public $attachmentManager;

    public function __construct(
        \Magetrend\Attachment\Model\AttachmentManager $attachmentManager
    ) {
        $this->attachmentManager = $attachmentManager;
    }

    public function beforeSetTemplateIdentifier($subject, $templateId)
    {
        $this->attachmentManager->resetParts();
        $this->attachmentManager->setTemplateId($templateId);
        return [$templateId];
    }

    public function beforeSetTemplateVars($subject, $templateVars)
    {
        $this->attachmentManager->setTemplateVars($templateVars);
        return [$templateVars];
    }
}

3. Add attachment to email

I should mention that we didn’t have an attachment part yet, but we will create it in the next step. In this step we will add our attachment to transactional email.

I have found that it’s possible to do this with a plugin for class:

Magento\Framework\Mail\MimeMessage

So we need to append our di.xml file, which was created in the previous step. The complete di.xml file should look like:

app/code/Magetrend/Attachment/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">

    <type name="Magento\Framework\Mail\Template\TransportBuilder">
        <plugin name="magetrend-attachment-transport-builder" type="Magetrend\Attachment\Plugin\TransportBuilder"/>
    </type>

    <type name="Magento\Framework\Mail\MimeMessage">
        <plugin name="magetrend-attachment-mimemessage" type="Magetrend\Attachment\Plugin\MimeMessage"/>
    </type>

</config>

4. Create an email attachment

Here is where all magic happens. We have already created all the plugins we need. We also have template id, template vars which are stored in attachmentManager object. All that is left is to identify the right template and create an email attachment part.

As an example, I choose the “New Invoice” template and we will attach Invoice PDF to it. It’s probably the most common case when you might want to attach a file to email..

Here is the attachmentManager class. Below I will overview it piece by piece.

app/code/Magetrend/Attachment/Model/AttachmentManager.php
<?php

namespace Magetrend\Attachment\Model;

use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity;

class AttachmentManager
{
    public $scopeConfig;

    public $invoicePdf;

    public $mimePartInterfaceFactory;

    public function __construct(
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Sales\Model\Order\Pdf\Invoice $invoicePdf,
        \Magento\Framework\Mail\MimePartInterfaceFactory $mimePartInterfaceFactory
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->invoicePdf = $invoicePdf;
        $this->mimePartInterfaceFactory = $mimePartInterfaceFactory;
    }

    private $templateId;

    private $templateVars = [];

    private $parts = null;

    public function setTemplateId($templateId)
    {
        $this->templateId = $templateId;
    }

    public function setTemplateVars($templateVars)
    {
        $this->templateVars = $templateVars;
    }

    public function getTemplateId()
    {
        return $this->templateId;
    }

    public function getTemplateVars()
    {
        return $this->templateVars;
    }

    public function resetParts()
    {
        $this->parts = null;
    }

    public function getParts()
    {
        return $this->parts;
    }

    public function addPart($part)
    {
        $this->parts[] = $part;
    }

    public function collectParts()
    {
        $this->parts = [];
        $invoiceTemplateId = $this->getConfigValue(
            InvoiceIdentity::XML_PATH_EMAIL_TEMPLATE,
            $this->getStoreId()
        );

        $guestInvoiceTemplateId = $this->getConfigValue(
            InvoiceIdentity::XML_PATH_EMAIL_GUEST_TEMPLATE,
            $this->getStoreId()
        );

        switch ($this->getTemplateId()) {
            case $invoiceTemplateId:
            case $guestInvoiceTemplateId:
                $this->attachInvoicePDF();
                break;
        }
    }

    public function getStoreId()
    {
        $vars = $this->getTemplateVars();
        if (!isset($vars['store'])) {
            return null;
        }

        $store = $vars['store'];
        return $store->getId();
    }

    public function getConfigValue($path, $store = null)
    {
        return $configValue = $this->scopeConfig->getValue(
            $path,
            \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
            $store
        );
    }

    public function attachInvoicePDF()
    {
        $vars = $this->getTemplateVars();
        $invoice = $vars['invoice'];

        $fileContent = $this->invoicePdf->getPdf([$invoice])->render();
        $fileName = 'invoice.pdf';

        $attachmentPart = $this->mimePartInterfaceFactory->create(
            [
                'content' => $fileContent,
                'type' => 'application/pdf',
                'fileName' => $fileName,
                'disposition' => \Zend\Mime\Mime::DISPOSITION_ATTACHMENT,
                'encoding' => \Zend\Mime\Mime::ENCODING_BASE64
            ]
        );

        $this->addPart($attachmentPart);
    }
}

Ok, now, about that in details. The following methods are simple setters and getters used instead of Magento 2 Registry class. It’s deprecated now.

public function setTemplateId($templateId)
public function setTemplateVars($templateVars)
public function getTemplateId()
public function getTemplateVars()

The next method resetParts will flush all our parts and lets re-collect attachments again. We need this method for email chains. Without it, all emails would have the same attachments.

public function resetParts()

In the method collectParts, we have to identify template and prepare attachments for current template. To identify email template we need to compare template id with template id, which is set in configuration.

public function collectParts()
{
    $this->parts = [];

    //get new invoice template id from configuration
    $invoiceTemplateId = $this->getConfigValue(
        InvoiceIdentity::XML_PATH_EMAIL_TEMPLATE,
        $this->getStoreId()
    );

    //get new invoice for guest template id from configuration
    $guestInvoiceTemplateId = $this->getConfigValue(
        InvoiceIdentity::XML_PATH_EMAIL_GUEST_TEMPLATE,
        $this->getStoreId()
    );

    //compare templates id
    switch ($this->getTemplateId()) {
        case $invoiceTemplateId:
        case $guestInvoiceTemplateId:
            $this->attachInvoicePDF();
            break;
    }
}

We also shouldn’t forget about the stores. Different stores can have a different template id. Current store we can always get from template variables. It’s implemented in method getStore()

public function getStoreId()
{
    $vars = $this->getTemplateVars();
    //check is store set
    if (!isset($vars['store'])) {
        return null;
    }

    //extract store from variables array
    $store = $vars['store'];
    return $store->getId();
}

And the final piece is method attachInvoicePDF. In this method, we will extract invoice PDF and create email attachment part from it. The “New Invoice” email has invoice data in variable list with key “invoice”. So we can easily take the data we need and generate invoice PDF. When we have a file content, we can create an attachment part and add it to our registry.

public function attachInvoicePDF()
{
    $vars = $this->getTemplateVars();
    //getting invoice from variable list
    $invoice = $vars['invoice'];
    
    //creating an invoice
    $fileContent = $this->invoicePdf->getPdf([$invoice])->render();
    $fileName = 'invoice.pdf';

    //creating email attachment part
    $attachmentPart = $this->mimePartInterfaceFactory->create(
        [
            'content' => $fileContent,
            'type' => 'application/pdf',
            'fileName' => $fileName,
            'disposition' => \Zend\Mime\Mime::DISPOSITION_ATTACHMENT,
            'encoding' => \Zend\Mime\Mime::ENCODING_BASE64
        ]
    );

    //add a part to registry
    $this->addPart($attachmentPart);
}

P.S. You can create more than one attachment here. You might want to attach the “Terms & Conditions” PDF here too. It can be added in the same method attachInvoicePDF.

You can download this email attachments extension source, which lets you attach PDF of an invoice to the “New Invoice” and “New Invoice for Guest” from github.

How to Attach a File via Magento 2 admin

If you’re not very technical or just looking for an easy solution, then you need an extension which would let you control email attachments via Magento 2 admin side. Luckily, we have a great  Magento 2 Email Attachments extension. 

It’s easy to control all your attachments using this module. They all are in one place. Here are a few more extension features, which makes it great: 

  • Possibile to load custom file
  • It’s working with Invoice, Order, Shipment, Credit Memo PDFs
  • Multiple attachments can be attached to the same template
  • The same attachment can be assigned to all email templates at once.

Conclusion

For now, it’s my favorite method of attaching the files to a transactional email in Magento 2. It’s very flexible and once you will implement it to your store, you can attach files to any email template. 

That’s all for this post. Let me know how it goes using it. If you will notice some compatibility issue, I will wait your feedback.

Happy coding guys!

Leave a Comment