Intégrer automatiquement un mail via les styles CSS avec Drupal 8

couleurs

Comment mettre des couleurs dans vos mails émis depuis votre site Drupal 8 ? Bien souvent, intégrer les mails émis par un site Drupal 8, ou de n'importe quel site web, peut s'avérer (très) chronophage, avec des contraintes fortes au niveau du rendu du mail pour disposer d'un rendu correct sur tous les types de courrielleur ou webmail. Sans parler ici du comportement responsive de ces mails, ne serait-ce par exemple que pour respecter la charte graphique d'un projet web (les boutons Call to action aux couleurs primaires du site, etc.).

Une des méthodes les plus efficaces consiste injecter dans le corps du mail, en HTML, les styles CSS à utiliser au niveau de chaque balise HTML, ce que nous appelons de l'inline CSS.

Ainsi pour intégrer par exemple un lien bouton, au lieu d'utiliser ce type de balise

<a class="btn btn-primary" href="https://www.flocondetoile.fr">Mon lien</a>

Nous devons plutôt utiliser ce type de balise avec les styles CSS inclus directement

<a href="https://www.flocondetoile.fr" style="padding: 6px 12px; color:#ff0000; border: 2px solid #ff0000; border-radius: 20px;">Mon lien</a>

Notation qui s'avère beaucoup moins souple et modulaire en matière de rendu.

Pour y parvenir, nous pouvons alors soit

  • Développer pour chaque type de mail émis un template spécifique et sur mesure, ce qui s'avère plutôt fastidieux et surtout définit à l'avance la position des éléments de contenu que le mail contiendra.
  • Automatiser la transformation des classes CSS utilisées, et ainsi conserver le rendu original sur le site, en styles CSS inline.

C'est bien sûr cette dernière option que je vais plus détailler ici.

Mettez des couleurs dans vos mails

Pour parvenir à nos fins, nous n'allons pas réinventer l'eau tiède et nous allons utiliser une librairie PHP très efficace sur ce point, à savoir Emogrifier.

Pour ajouter cette librairie à notre projet, nous utilisons cette commande Composer

composer require pelago/emogrifier

Afin de pouvoir Emogrifier tous types de mails émis, nous allons construire un service qui pourra être appelé autant que de besoin.

Dans le fichier my_module.services.yml, du module MY_MODULE déclarons ce service.

my_module.emogrify:
  class: Drupal\my_module\Emogrify
  arguments: [ '@config.factory', '@theme.manager', '@file_system', '@library.discovery', '@theme_handler', '@module_handler']

Puis déclarons notre Class Emogry dans le répertoire src/ du module. Le concept de ce service repose avant tout, pour utiliser correctement la librairie Emogrifier, sur la récupération des fichiers CSS à utiliser pour traiter une chaine HTML et transformer toutes les classes CSS de l'HTML fourni en style CSS inline. Vous pouvez bien sûr utiliser le fichier CSS de votre thème principal, mais selon sa taille, pour des raisons de performance, il peut être plus pertinent de fournir un fichier CSS dédié au theming des mails (comme par exemple mail.css). Et cela vous permet également de gérer différemment certains comportements CSS, au survol par exemple, ainsi que de mieux cibler le rendu (certains conflits peuvent apparaitre en fonction de multiples selecteurs CSS aux comportements différents qui seraient inliner dans la même balise HTML).

Cette Class va donc disposer d'une méthode principale emogrify(), à mettre dans l'interface de la Class, puis de différentes autres méthodes, dites d'aide, pour récupérer les bons fichiers CSS, en fonction de votre projet, méthodes que vous devrez très certainement adapter à votre projet.

Par exemple, la méthode getCssOverride() récupère le fichier CSS fourni par un module custom, et en cas d'absence de ce fichier, nous effectuons un fallback pour récupérer les fichiers CSS de la librairie 'mail' du thème par défaut, avec la méthode getCssTheme(). A noter que nous supportons aussi le module Asset Injector, en récupérant tous les CSS injector créés qui contiennent la chaine 'mail' dans leur identifiant, avec la méthode getCssInjector(). Bien entendu cela peut être affiné en fonction des besoins de chaque projet.

Voici donc le code complet à titre d'exemple de notre Class Emogriphy.

namespace Drupal\my_module;

use Drupal\Core\Asset\LibraryDiscoveryInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Theme\ThemeManagerInterface;
use Pelago\Emogrifier\CssInliner;

class Emogrify implements EmogrifyInterface {

  /**
   * Drupal\Core\Config\ConfigFactoryInterface definition.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected  $configFactory;

  /**
   * Drupal\Core\Theme\ThemeManagerInterface definition.
   *
   * @var \Drupal\Core\Theme\ThemeManagerInterface
   */
  protected  $themeManager;

  /**
   * Drupal\Core\File\FileSystemInterface definition.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * Drupal\Core\Asset\LibraryDiscoveryInterface definition.
   *
   * @var \Drupal\Core\Asset\LibraryDiscoveryInterface
   */
  protected $libraryDiscovery;

  /**
   * Drupal\Core\Extension\ThemeHandlerInterface definition.
   *
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
  protected $themeHandler;

  /**
   * Drupal\Core\Extension\ModuleHandlerInterface definition.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Emogrify constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   */
  public function __construct(ConfigFactoryInterface $config_factory, ThemeManagerInterface $theme_manager, FileSystemInterface $file_system, LibraryDiscoveryInterface $library_discovery, ThemeHandlerInterface $theme_handler, ModuleHandlerInterface $module_handler) {
    $this--->configFactory = $config_factory;
    $this->themeManager = $theme_manager;
    $this->fileSystem = $file_system;
    $this->libraryDiscovery = $library_discovery;
    $this->themeHandler = $theme_handler;
    $this->moduleHandler = $module_handler;
  }

  /**
   * Gets the default theme.
   *
   * @return string
   *   The default theme.
   */
  protected function getDefaultTheme() {
    $default_theme = $this->configFactory->get('system.theme')->get('default');
    return $default_theme;
  }

  /**
   * Get all the libraries given a theme.
   *
   * @param $theme
   * @return array
   */
  protected function getThemeLibraries($theme) {
    $libraries = [];
    $themes = $this->themeHandler->rebuildThemeData();
    $root = \Drupal::root();

    if (isset($themes[$theme])) {
      /** @var \Drupal\Core\Extension\Extension $extension */
      $extension = $themes[$theme];
      $library_file = $extension->getPath() . '/' . $theme . '.libraries.yml';
      if (is_file($root . '/' . $library_file)) {
        $libraries = $this->libraryDiscovery->getLibrariesByExtension($theme);
      }
    }
    return $libraries;
  }

  /**
   * Gets the libraries to use for the CssInliner.
   *
   * @return array
   */
  protected function getCssLibraries() {
    return [
      'mail',
    ];
  }

  /**
   * Gets the css to apply on mail.
   *
   * @return string
   */
  protected function getCss($theme) {
    $css = $this->getCssOverride();
    if (empty($css)) {
      $css = $this->getCssTheme($theme);
    }
    $css_injector = $this->getCssInjector();
    if (!empty($css_injector)) {
      $css = $css . PHP_EOL . $css_injector;
    }
    return $css;
  }

  /**
   * Gets the css from default theme.
   *
   * @return string
   */
  protected function getCssTheme($theme) {
    $css = '';
    $libraries = $this->getThemeLibraries($theme);
    $css_libraries = $this->getCssLibraries();
    foreach ($css_libraries as $css_library) {
      if (isset($libraries[$css_library]['css'])) {
        $theme_css_files = $libraries[$css_library]['css'];
        foreach ($theme_css_files as $key => $theme_css_file) {
          if (empty($theme_css_file['data'])) {
            continue;
          }
          $data = $theme_css_file['data'];
          $data_file = $this->fileSystem->realpath($data);
          $css = $css . PHP_EOL . file_get_contents($data_file);
        }
      }
    }
    return $css;
  }

  /**
   * Gets the css override from usine_theme module.
   *
   * @return string
   */
  protected function getCssOverride() {
    $css = '';
    if ($this->moduleHandler->moduleExists('usine_theme')) {
      /** @var \Drupal\usine_theme\ManagerAssetInterface $manager */
      $manager = \Drupal::service('usine_theme.manager');
      if ($manager->hasAssetOverride('color')) {
        $data = $manager->getAsset('mail');
        if ($data) {
          $data = ltrim($data, '/');
          $data_file = $this->fileSystem->realpath($data);
          $css = file_get_contents($data_file);
        }
      }
    }
    return $css;
  }

  /**
   * Gets the css for asset_injector module.
   *
   * @return string
   */
  protected function getCssInjector() {
    $css = '';
    if ($this->moduleHandler->moduleExists('asset_injector')) {
      foreach (asset_injector_get_assets(TRUE, ['asset_injector_css']) as $asset) {
        if (strpos($asset->id(), 'mail') !== FALSE) {
          $css_content = $asset->getCode();
          $css = $css . PHP_EOL . $css_content;
        }
      }
    }
    return $css;
  }

  /**
   * {@inheritdoc}
   */
  public function emogrify($html, $theme = 'default') {
    if ($theme === 'default') {
      $theme = $this->getDefaultTheme();
    }
    if ($html instanceof Markup) {
      $html = (string) $html;
    }
    if (!is_string($html)) {
      return $html;
    }
    $css = $this->getCss($theme);
    if (empty($css)) {
      return $html;
    }
    Try {
      $html_inline_css = CssInliner::fromHtml($html)->inlineCss($css)->renderBodyContent();
    }
    catch (\Exception $e) {
      return $html;
    }
    $html = Markup::create($html_inline_css);
    return $html;
  }
}

Il ne nous reste plus qu'à utiliser notre service pour automatiquement remplacer toutes les classes CSS contenues dans le corps du mail par leur style inline CSS correspondant qui aura été trouvé dans les fichiers CSS fournis.

Une implémentation de hook_mail_alter() sera suffisante.

/**
 * Implements hook_mail_alter().
 */
function my_module_mail_alter(&$message) {
  // Tell to SwiftMailer to use our template for email body.
  $message['params']['theme'] = 'mail_base';
  /** @var \Drupal\my_module\EmogrifyInterface $emogrify */
    $emogrify = \Drupal::service('my_module.emogrify');
    if (!empty($message['body'])) {
      foreach ($message['body'] as $key => $body) {
        $body = $emogrify->emogrify($body);
        $message['body'][$key] = new FormattableMarkup($body, []);
    }
  }
}

Nous pouvons alors simplement enrichir le contenu des mails émis pour notre projet Drupal 8, voire personnaliser au fur et à mesure des besoins le rendu des mails, par l'ajout de règles CSS dans notre librairie ou encore via CSS injector, selon le contexte. Et ceci peut signifier, au moins pour une bonne part, la fin de l'angoisse du développeur Drupal 8 dès qu'il s'agit d'intégrer un nouveau type de mail pour son projet.

Prenez soin de vous.

 

Ajouter un commentaire