bgaze
bgaze

Reputation: 1000

Prestashop 1.6 - Using emails hooks

I am currently building a shop using Prestashop 1.6.1.5.

As emails I must send are numerous, I would like to manage my email skeletton in a single place.
I think that the easier way would be to manage emails into smarty files and to [extend my layout][1].

Does anyone now a transparent and global way to manage email layouts?

Thanks,

Ben.

[EDIT]

Diving into MailCore class, I saw that email templates can be manipulated before send using three hooks (simplified and commented) :

// Init empty templates strings.
$template_html = '';
$template_txt = '';

// Manipulate strings before importing templates.
Hook::exec('actionEmailAddBeforeContent', array(
    'template_html' => &$template_html,
    'template_txt' => &$template_txt,
    // ...
), null, true);

// Import templates.
$template_html .= Tools::file_get_contents(/* ... */);
$template_txt .= strip_tags(html_entity_decode(Tools::file_get_contents(/* ... */), null, 'utf-8'));

// Manipulate strings after importing templates.
Hook::exec('actionEmailAddAfterContent', array(
    'template_html' => &$template_html,
    'template_txt' => &$template_txt,
    // ...
), null, true);

// Some MailCore stuff.

// Inject custom vars before generate email content
$extra_template_vars = array();
Hook::exec('actionGetExtraMailTemplateVars', array(
    'template_vars' => $template_vars,
    'extra_template_vars' => &$extra_template_vars,
    // ...
), null, true);
$template_vars = array_merge($template_vars, $extra_template_vars);

// Generate and send email

So it seems to me that using these hooks is the good way to manage my email layout, but I don't get how it's work to define the function called by hooks.

To make it global, I tried to override MailCore (into /override/classes/Mail.php) :

class Mail extends MailCore {

    public function __construct($id = null, $id_lang = null, $id_shop = null) {
        parent::__construct($id, $id_lang, $id_shop);
        PrestaShopLogger::addLog('MailCore overrided!');
    }

    // Prepend a header to template.
    public function hookActionEmailAddBeforeContent($params) {
        PrestaShopLogger::addLog('hookActionEmailAddBeforeContent called!');
        $params['template_html'] .= '<h1>{myheader}</h1>';
    }

    // Append a footer to template.
    public function hookActionEmailAddAfterContent($params) {
        PrestaShopLogger::addLog('hookActionEmailAddAfterContent called!');
        $params['template_html'] .= '<h1>{myfooter}</h1>';
    }

    // Add my custom vars.
    public function hookActionGetExtraMailTemplateVars($params) {
        PrestaShopLogger::addLog('hookActionGetExtraMailTemplateVars called!');
        $params['extra_template_vars']['myheader'] = 'This is a header';
        $params['extra_template_vars']['myfooter'] = 'This is a footer';
    }

}

After clearing Prestashop cache, class override works (log from constructor) but none of my hooks is called.
I also tried to register hooks using $this->registerHook('...'); into my class constructoe, but without any effect.

If anyone could help, It would be very great.

Thanks,

Ben.

Upvotes: 0

Views: 1406

Answers (2)

bgaze
bgaze

Reputation: 1000

@TheDrot : Thank for these usefull explanations :-)

As TheDrot pointed it out, this emails thing is kinda messy. I want to :

  • be able to edit emails from BO.
  • manage email layouts globally (core, modules, custom, ...)
  • have several layout and select the one I will use directly into template.

So I decided to override MailCore class and to implement following basic but working "layout extension" system.
Of course, the main drawback of this solution is that I will have to keep my overrided function up to date when updating Prestashop.

I am in a single language context and I don't need layouts for text emails, but managing multiple language and texts emails is easy too.

Following code can of course be highly improved, it's just a quick demo.

Layouts

Layouts are placed into /themes/{theme}/mails/layouts dir.
Any variable available into the email can be used and content place is defined using {{CONTENT}} tag.

/themes/mytheme/mails/layouts/my-layout.html :

<h1>A header</h1>
{{CONTENT}}
<h1>A footer</h1>

Email template

Into email template, layout to inherit from is defined using {extends:name-of-layout} tag :

/themes/mytheme/mails/en/password_query.html :

{{extends:my-layout}}

<p>
    <b>
        Hi {firstname} {lastname},
    </b>
</p>
<p>
    You have requested to reset your {shop_name} login details.<br/>
    Please note that this will change your current password.<br/>
    To confirm this action, please use the following link:
</p>
<p>
    <a href="{url}" class="button">Change my pasword</a>
</p>

Mail class

Here is the main function : if "extends" tag exists into template and required layout is founded, populate layout with template.

/override/classes/Mail.php :

public static function layout($theme_path, $template, $content) {
    preg_match("/^\{\{extends\:(\w+)\}\}/", ltrim($content), $m);
    if (!isset($m[1]) || !file_exists($theme_path . 'mails/layout/' . $m[1] . '.html')) {
        return $content;
    }

    $content = ltrim(str_replace('{{extends:' . $m[1] . '}}', '', $content));
    $layout = Tools::file_get_contents($theme_path . 'mails/layout/' . $m[1] . '.html');

    return str_replace('{{CONTENT}}', $content, $layout);
}

Then Send function is modified in a single place to apply layout function to template :

public static function Send($id_lang, $template, $subject, $template_vars, $to, $to_name = null, $from = null, $from_name = null, $file_attachment = null, $mode_smtp = null, $template_path = _PS_MAIL_DIR_, $die = false, $id_shop = null, $bcc = null, $reply_to = null) {

    // ...

    $template_html = '';
    $template_txt = '';
    Hook::exec('actionEmailAddBeforeContent', array(
        'template' => $template,
        'template_html' => &$template_html,
        'template_txt' => &$template_txt,
        'id_lang' => (int) $id_lang
            ), null, true);
    $template_html .= Tools::file_get_contents($template_path . $iso_template . '.html');
    $template_txt .= strip_tags(html_entity_decode(Tools::file_get_contents($template_path . $iso_template . '.txt'), null, 'utf-8'));
    Hook::exec('actionEmailAddAfterContent', array(
        'template' => $template,
        'template_html' => &$template_html,
        'template_txt' => &$template_txt,
        'id_lang' => (int) $id_lang
            ), null, true);

    // Apply self::layout function to template when acquired.
    $template_html = self::layout($theme_path, $template_path . $iso_template . '.html', $template_html);

    // ...
}

Upvotes: 1

TheDrot
TheDrot

Reputation: 4337

For custom mails only you don't need hooks or override anything. All you have to do here before calling Mail::send() is that you generate three template vars through smarty {header}, {content}, {footer}.

public function someModuleMailFunc($email_template)
{
    $header_tpl = $this->context->smarty->createTemplate('path_to_header_tpl_smarty');
    $header_tpl->assign(array(
        // assign header template vars
    ));

    // put some conditionals here to load a proper content for specific mail
    if ($email_template == 'some_mail_template') {    
        $content_tpl = $this->context->smarty->createTemplate('path_to_content_tpl_smarty');
        $content_tpl->assign(array(
            // assign content template vars
        ));
    }
    $footer_tpl = $this->context->smarty->createTemplate('path_to_footer_tpl_smarty');
    $footer_tpl->assign(array(
        // assign footer template vars
    ));

    $email_vars = array(
        '{header}' => $header_tpl->fetch(),
        '{content}' => $content_tpl->fetch(),
        '{footer}' => $footer_tpl->fetch()        
    );

    Mail::send('en', $email_template, $subject, $email_vars, $to); 
}

Now make sure you have your some_email_template.html structure like this.

{header}
{content}
{footer}

You also don't need to have email templates for each language since you use smarty templates which can call language functions {l s='text'}.

And the example is for html templates only, add code that handles txt templates.

To change core emails... now this is where things complicate.

One way is to hook onto actionEmailAddAfterContent and actionGetExtraMailTemplateVars.

public function hookActionEmailAddAfterContent($params)
{
    // here we can replace the email templates with {header} {content} {footer}
    // this param is passed by reference
    $params['template_html'] = '{header}{content}{footer}';
}

public function hookActionGetExtraMailTemplateVars($params)
{
    // here we generate vars for {header} {content} {footer}

    $header_tpl = $this->context->smarty->createTemplate('path_to_header_tpl_smarty');
    $header_tpl->assign(array(
        // assign header template vars
    ));

    // existing template vars can be accessed with $params['template_vars']
    // so they can be reinserted into your templates

    // put some conditionals here to load a proper content for specific mail
    if ($params['template'] == 'some_mail_template') {    
        $content_tpl = $this->context->smarty->createTemplate('path_to_content_tpl_smarty');
        $content_tpl->assign(array(
            // assign content template vars
        ));
    }
    $footer_tpl = $this->context->smarty->createTemplate('path_to_footer_tpl_smarty');
    $footer_tpl->assign(array(
        // assign footer template vars
    ));

    // this param is also passed by reference
    $params['extra_template_vars']['{header}'] = $header_tpl->fetch();
    $params['extra_template_vars']['{content}'] = $content_tpl->fetch();
    $params['extra_template_vars']['{footer}'] = $footer_tpl->fetch();
}

This way the extra params will be picked up by Mail::send() and since we modified the template before, it will insert header content and footer into it.

Problem with this is that you will still need to add email template files for each language even though they are never used because Mail::send() still tries to load them before these hooks are executed. Unless you want to override the method of course.

Also editing emails through backoffice for managers becomes useless since they wont see anything but {header}{content}{footer} for your custom module mails and editing core mails will have no effect.

Whole thing with email templates is kinda messy, I don't know why the devs decided to create mail system this way and not use smarty. Maybe for easier implementation of backoffice email editing... I don't know. Perhaps if any prestashop dev sees this they might explain it.

Upvotes: 0

Related Questions