03/03/2021

File d’attente pour les mails avec symfony

Envoyer des mails avec symfony ou même avec n’importe quelle technologie peut être assez long. La technique que je vais vous montrer s’appelle une queue. Le principe est de déposer les informations dans une base de données et une tâche de font va les envoyer en mail de façon asynchrone.

Envoi de mail

Configuration

La première étape est de faire la partie en voie de mail :

Dans le fichier .env, rajoutez ces variables

SMTP_HOST=outlook.office365.com
SMTP_USER=blablabl@outlook.com
SMTP_PASSWORD=XXXXXX
SMTP_PORT=587

Ces variables seront accessible de cette manière :

echo $_ENV['SMTP_HOST'];

Mail generator

Fonctionne avec PHPMailer

composer require phpmailer/phpmailer

Ensuite voici la classe qui permet de faire un mail.

<?php
// src/Service/MailGenerator.php

namespace App\Service;

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use App\Entity\MailNotify;
use App\Entity\QueueMail;

class MailGenerator
{
    public $content;

    public function createMailFromQueueMail($queueMail)
    {
        $this->content = [
            'subject' => $queueMail->getName(),
            'body' => $queueMail->getDescription(),
            'altBody' => $queueMail->getDescription(),
            'recipients' => [$queueMail->getDestination()],
        ];

        return $this;
    }

    public function sendMail()
    {
        $mail = new PHPMailer(true);


        try {

            //Server settings
            $mail->SMTPDebug = 0;                                       // No OUTPUT
            $mail->isSMTP();                                            // Set mailer to use SMTP
            $mail->Host       = $_ENV['SMTP_HOST'];  // Specify main and backup SMTP servers
            $mail->SMTPAuth   = true;                                   // Enable SMTP authentication
            $mail->Username   = $_ENV['SMTP_USER'];                     // SMTP username
            $mail->Password   = $_ENV['SMTP_PASSWORD'];                               // SMTP password
            $mail->SMTPSecure = 'tls';                                  // Enable TLS encryption, `ssl` also accepted
            $mail->Port       = $_ENV['SMTP_PORT'];                                    // TCP port to connect to

            //Recipients
            $mail->setFrom($_ENV['SMTP_USER']);

            foreach ($this->content['recipients'] as $recipien)
                $mail->addAddress($recipien);     // Add a recipien

            // Content
            $mail->isHTML(true);                                  // Set email format to HTML
            $mail->Subject = '[toto] ' . $this->content['subject'];
            $mail->Body    = $this->content['body'];
            $mail->AltBody = $this->content['altBody'];

            $mail->send();
            return true;

        } catch (Exception $e) {
            echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}\n";
            return false;
        }

    }
}

Queue Mail

Voici l’entité QueueMail qui représente les mails en attente :

L’entité

<?php
// src/Entity/QueueMail.php

namespace App\Entity;

use App\Repository\QueueMailRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=QueueMailRepository::class)
 */
class QueueMail
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $description;

    /**
     * @ORM\Column(type="datetime")
     */
    private $createdAt = null;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $destination;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeInterface $createdAt): self
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    public function getDestination(): ?string
    {
        return $this->destination;
    }

    public function setDestination(string $destination): self
    {
        $this->destination = $destination;

        return $this;
    }
}

Le repository

<?php
// src/Repository/QueueMailRepository.php

namespace App\Repository;

use App\Entity\QueueMail;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @method QueueMail|null find($id, $lockMode = null, $lockVersion = null)
 * @method QueueMail|null findOneBy(array $criteria, array $orderBy = null)
 * @method QueueMail[]    findAll()
 * @method QueueMail[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class QueueMailRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, QueueMail::class);
    }

    // /**
    //  * @return QueueMail[] Returns an array of QueueMail objects
    //  */
    /*
    public function findByExampleField($value)
    {
        return $this->createQueryBuilder('q')
            ->andWhere('q.exampleField = :val')
            ->setParameter('val', $value)
            ->orderBy('q.id', 'ASC')
            ->setMaxResults(10)
            ->getQuery()
            ->getResult()
        ;
    }
    */

    /*
    public function findOneBySomeField($value): ?QueueMail
    {
        return $this->createQueryBuilder('q')
            ->andWhere('q.exampleField = :val')
            ->setParameter('val', $value)
            ->getQuery()
            ->getOneOrNullResult()
        ;
    }
    */
}

Remplir la queue

Rajouter ce code là où normalement vous envoyiez le mail. Il va juste remplir la queue

use App\Entity\QueueMail;
...

// Envoi de mail
$mail = new QueueMail();
$mail->setName("Nom");
$mail->setDescription("Bonjour, voici le nouveau mail a envoyer");
$mail->setDestination("blablabl@mail.fr");
$mail->setCreatedAt(new \DateTime());

// Sauvegarde 
$entityManager->persist($mail);
$entityManager->flush();

Command envoie de mail

Le but de cette partie et de créer une commande symfony qui permet de vider la queue de mail

php bin/console mail:empty

Voici le code

<?php
// src/Command/EmptyQueueMail.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use App\Entity\QueueMail;
use App\Repository\QueueMailRepository;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\MailGenerator;

class EmptyQueueMail extends Command
{
    // the name of the command (the part after "bin/console")
    protected static $defaultName = 'mail:empty';

    protected $entityManager;
    protected $queueMailRepository;

    public function __construct(EntityManagerInterface $entityManager, QueueMailRepository $queueMailRepository)
    {
        parent::__construct();
        $this->entityManager = $entityManager;
        $this->queueMailRepository = $queueMailRepository;
    }

    protected function configure()
    {
        $this
            // the short description shown while running "php bin/console list"
            ->setDescription('Send mails from queue')

            // the full command description shown when running the command with
            // the "--help" option
            ->setHelp('This command allows you to empty the mail list.')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $queueMails = $this->queueMailRepository->findAll();

        $nbMail = count($queueMails);

        if ($nbMail < 1) 
        {
            $output->writeln('No mail pending.');
            return 0;
        }

        $output->writeln([
            "There are $nbMail mail(s) pending mail.",
            '=============================================',
            '',
        ]);

        $error = 0; // number of error

        foreach ($queueMails as $queueMail) 
        {
            $mail = new MailGenerator();
            $mail->createMailFromQueueMail($queueMail);
            if($mail->sendMail())
            {
                // Suppression des mails
                $this->entityManager->remove($queueMail);

                $status = 'Send';
            }
            else
            {   
                $status = 'Error';
                $error++;
            }
            
            $output->writeln($status . ' : ' . $queueMail->getName() . " to " . $queueMail->getDestination());
            
        }


        $this->entityManager->flush();
        $output->writeln($nbMail - $error . " mail(s) sent and $error error(s)");

        return 0;
    }
}

Automatisation (crontab)

Toutes les heures

0 * * * * php /var/www/html/webapp/bin/console mail:empty