I want to send emails in my symfony app both in sync and async mode. I followed the documentation of symfony messenger, but I have an issue with one key parameter : routing
framework:
messenger:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'Symfony\Component\Mailer\Messenger\SendEmailMessage': async
With this configuration, all messages are sent in queue.
I have 2 services where I send emails sync in the first and async in the second :
<?php
namespace App\Service;
use Symfony\Component\Mime\Address;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;
class MailManagerAsync
{
protected $mailer;
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
protected function sendMessage($subject, $body, $bodyText, $to, $context=[])
{
$email = (new TemplatedEmail())
->from($this->from)
->to(new Address($to))
->subject($subject)
// path of the Twig template to render
->htmlTemplate($body)
->textTemplate($bodyText)
// pass variables (name => value) to the template
->context($context)
;
$this->mailer->send($email);
}
// function where i send emails
}
The second is the same as this first, called MailManagerSync.
Now here is the problem :
when i change routing like this :
routing : 'App\Service\MailManagerAsync': async
the app is sending 2 emails, 1 queued and one sent. I would like to send only one QUEUED.
Any suggestions ?
EDIT : I understand that I have to create a message class, and send it to the bus, but:
is there any easy solution to have both sync and async emails without having to develop all I said above ?
Ok here is a solution that could help other people. I think I did not come with it beacause this messenger thing is relatively new.
First create a message class :
<?php
namespace App\Message;
class EmailAsync
{
private $subject;
private $body;
private $bodyText;
private $recipient;
private $context = [];
public function __construct($subject, $body, $bodyText, $recipient, $context)
{
$this->subject = $subject;
$this->body = $body;
$this->bodyText = $bodyText;
$this->recipient = $recipient;
$this->context = $context;
}
// setters and getter
This class will transport the email.
Then here is the handler :
<?php
namespace App\MessageHandler;
use App\Message\EmailAsync;
use Symfony\Component\Mime\Address;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class EmailAsyncHandler implements MessageHandlerInterface
{
protected $mailer;
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
public function __invoke(EmailAsync $email)
{
$subject = $email->getSubject();
$body = $email->getBody();
$bodyText = $email->getBodyText();
$recipient = $email->getRecipient();
$context = $email->getContext();
$emailToSend = (new TemplatedEmail())
->from("my-address#hello.com")
->to(new Address("your-address#hello.com"))
->subject($subject)
// path of the Twig template to render
->htmlTemplate($body)
->textTemplate($bodyText)
// pass variables (name => value) to the template
->context($context)
;
$this->mailer->send($emailToSend);
}
}
and now you can set the right routing parameter :
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
# Route your messages to the transports
'App\Message\EmailAsync': async
Now if i want to send an async mail, i use the bus within my service :
<?php
namespace App\Service;
use App\Message\EmailAsync;
use Symfony\Component\Messenger\MessageBusInterface;
class MailManagerAsync
{
protected $bus;
public function __construct(MessageBusInterface $bus)
{
$this->bus = $bus;
}
protected function sendMessage($subject, $body, $bodyText, $to, $context = [])
{
$emailAsync = new EmailAsync($subject, $body, $bodyText, $to, $context);
$this->bus->dispatch($emailAsync);
}
// SEND EMAILS
}
Related
I have a Symfony project which (very simplified) looks like this:
Controller/MyToolController.php
namespace App\Controller;
use App\Services\ToolsService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class MyToolController extends AbstractController
{
private $toolsService;
/**
* #param ToolsService|null $toolsService
*/
public function __construct(ToolsService $toolsService = null) {
$this->toolsService = $toolsService;
}
public function run() {
// some more code here...
$mailContent = $this->render('site/mail.html.twig', $data)->getContent();
$this->toolsService->sendMail($from, $to, $bcc, $mailContent, $subject);
// if I remove the following line, no emails are sent out!
return $this->render('site/thankyou.html.twig', $data);
}
}
Services/MyToolService.php
namespace App\Services;
class ToolsService
{
/** #var \Swift_Mailer */
private $mailer;
/**
* #param \Swift_Mailer $mailer
*/
public function __construct(\Swift_Mailer $mailer)
{
$this->mailer = $mailer;
}
public function sendMail($from, $to, $bcc, $body, $subject) {
$mail = ( new \Swift_Message() )
->setSubject($subject)
->setFrom($from)
->setTo($to)
->setBcc($bcc)
->addPart($body, 'text/html');
$res = $this->mailer->send($mail);
$this->logger->info('Email sent');
return $res;
}
}
If you look at MyToolController.php you see that I call a service which sends the email.
If I return a Response object in my run() function, everything goes well - but if I omit this, nothing is sent. Same if I send multiple emails in a loop and I run into a timeout.
Strangely $mailer->send() is called in any case - and it returns 1 - and I see an entry in the log I am writing in the sendmail() function. But no email leaves the server.
This is my SwiftMailer configuration:
swiftmailer:
url: '%env(MAILER_URL)%'
spool: { type: 'memory' }
Your mails are being spooled in memory, which means:
When you use spooling to store the emails to memory, they will get sent right before the kernel terminates. This means the email only gets sent if the whole request got executed without any unhandled exception or any errors.
If you do not return a Response object from your controller, there will be an unhandled exception, the whole request won't be executed, and the mails will never be sent.
Controllers need to return a Symfony\Component\HttpFoundation\Response so the request can be handled normally. If you want to return an empty response, you can simply do:
return new Response();
I have a Mail.php file that contains a sendMail function that will be used by several of my controllers.
I got to have to use the "templating" service. But I have problems putting it in place.
My Services.yml:
email_management:
class: Site\PagesBundle\Utils\Mails
arguments: ['#templating']
public: true
My Mail.php:
<?php
namespace Site\PagesBundle\Utils;
use Site\PagesBundle\Entity\User;
use Site\PagesBundle\Entity\UserCas;
class Mails
{
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
public function sendMail($user,$raisonMail)
{
$transport = \Swift_SmtpTransport::newInstance();
$mailer = new \Swift_Mailer($transport);
// EntĂȘte
$message = \Swift_Message::newInstance()
->setFrom(array('############' => '############'))
//->setTo($user->getEmail());
->setTo("############")
->setCharset('utf-8')
->setContentType('text/html');
switch($raisonMail)
{
case 'formulaireInscription':
dump($user);
// (1) Confirmation de demande d'inscription
$message->setSubject("subject")
->setBody($this->templating->render("#Pages/swiftmail/CreationCompte/DemandeCreationCompte.html.twig",array(
'prenom'=>$user->getPrenom(),
'nom'=>$user->getNom(),
)));
break;
//... other cases
In my controller :
$templating = new EngineInterface;
$mail = new Mail($templating);
$mail->get('email_management')->sendEmail($user,$motif);
But now I've this error :
You must set a loader first.
Can someone help me please ? Thanks !
Assuming that the intention is to go for the service based option. Please note in general that the service class is intended to be moved into different folder in the project (to be under PagesBundle/Service folder).
services.yml (please note the changed path)
email_management:
class: Site\PagesBundle\Service\EmailManagementService
arguments: ['#templating']
public: true
EmailManagementService.php (please note the changed location & namespace)
<?php
namespace Site\PagesBundle\Service;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Site\PagesBundle\Entity\User;
use Site\PagesBundle\Entity\UserCas;
class Mails
{
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
...
}
Usage in controller:
$this->get('email_management')->sendMail($user,'formulaireInscription');
I'm trying to send an email with SMTP in Symfony 4 and I have the following function as per the docs.
public function send_smtp(\Swift_Mailer $mailer)
{
$message = (new \Swift_Message('Hello Email'))
->setFrom('send#example.com')
->setTo('MyEmail#gmail.com')
->setBody('test email content');
$mailer->send($message);
}
However, I want to call this function from a different one like
$this->send_smtp();
But it complains about 'No parameters are passed' and
$this->send_smtp(\Swift_Mailer);
also gives the error message
Undefined constant 'Swift_Mailer'
What can be the problem and how could it be solved?
There are a few solutions possible. You can add the parameter with typehinting in your action and then use it to call your function:
/**
* #Route("/something")
*/
public function something(\Swift_Mailer $mailer)
{
$this->send_smtp($mailer);
}
or you can retrieve the service from the container in the send_smtp function:
public function send_smtp()
{
$mailer = $this->get('mailer');
$message = (new \Swift_Message('Hello Email'))
->setFrom('send#example.com')
->setTo('MyEmail#gmail.com')
->setBody('test email content');
$mailer->send($message);
}
Create service e.g.
namespace App\Service\Mailer;
class Mailer
{
private $mailer;
public function __construct(\Swift_Mailer $mailer)
{
$this->mailer = $mailer;
}
public function send_smtp()
{
$message = (new \Swift_Message('Hello Email'))
->setFrom('send#example.com')
->setTo('MyEmail#gmail.com')
->setBody('test email content');
$this->mailer->send($message);
}
}
And now you can inject this service wherever you want (e.g. to your Controller action or in another service) via __construct:
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function someAction()
{
$this->mailer->send_smtp()
}
Or you can inject it via method or via property. You can read more about injections here: https://symfony.com/doc/current/components/dependency_injection.html
P.S. I don't recommend you use container's method get because this method works only for public services, but in Symfony 4 services are private by default.
For testing purpose, I want to send raw mail via Queue.
I can send a raw mail like this:
Mail::raw('bonjour', function($message) {
$message->subject('Email de test')
->to('test#example.org');
});
But is there a way to send a raw mail via Queue (without creating a View nor Mailable)?
You can use dispatch helper function to push Closure onto the Laravel job queue:
dispatch(function () use ($name) {
Mail::raw('bonjour ' . $name, function($message) {
$message->subject('Email de test')
->to('test#example.org');
});
});
I searched the past days without any outcome to accomplish exact this: a raw mail that can be queued.
Unfortunately I didn't found a solution without using Mailables and Views.
I assume you have the same reason as me: You want to send a 100% dynamically generated Mail from a string.
My solution was:
creating a view that only contains one variable: <?php echo $content;
create a mailable, passing the content to the constructor and set it to $this->content
copy everything inside the old mail-closure into the build-method of the mailable and replace every $message-> with $this
queue it ;)
public function send(Request $request) {
$to = "test#example.org";
$subject = "email de test";
$content = "bonjour";
Mail::send(new RawMailable($to, $subject, $content));
}
view (/ressources/view/emails/raw.blade.php):
{!! $content !!}
mailable:
<?php
namespace App\Mail;
use Dingo\Api\Http\Request;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class RawMailable extends Mailable
{
use Queueable, SerializesModels, ShouldQueue;
private $mailTo;
private $mailSubject;
// the values that shouldnt appear in the mail should be private
public $content;
// public properties are accessible from the view
/**
* Create a new message instance.
*
* #param LayoutMailRawRequest $request
*/
public function __construct($to, $subject, $content)
{
$this->content = $content;
$this->mailSubject = $subject;
$this->mailTo = $to;
}
/**
* Build the message.
*
* #throws \Exception
*/
public function build()
{
$this->view('emails.raw');
$this->subject($this->mailSubject)
->to($this->mailTo);
}
}
Leave the paramaters view and variables with an empty array each one and add the line $mail->setBody($html, 'text/html') inside the function.
Mail::queueOn(
'name_of_queue',
[],
[],
function($mail) use ($destination_email, $destination_name, $html) {
$mail->to($destination_email, $destination_name);
$mail->subject($subject);
$mail->setBody($html, 'text/html');
}
);
I am working with Symfony2 and am trying to access mailer service but constantly get this error message:
{"errors":{"code":500,"message":"Error: Call to a member function get() on a non-object"}}
my code:
<?php
namespace TestBundle\UserBundle\Utilities;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class EmailServiceClass extends Controller
{
public function sendEmail($subject, $to, $body)
{
$msg = \Swift_Message::newInstance();
$msg->setSubject($subject);
$msg->setTo($to);
$msg->setBody($body);
$msg->setContentType('text/html');
$msg->setCharset('utf-8');
$msg->setFrom('test#gmail.com');
$this->get('mailer')->send($msg);
}
}
The error comes from this line: $this->get('mailer')->send($msg);
From what I understand when I extend Controller calls I should be able to access this service without having to specifically create a service.
You should do it another way.
It's the best when your services are POPO (Plain Old PHP Object). Also dependencies should be passed via constructor, so let's refactor your service a little bit:
class EmailServiceClass //no need to extend anything
{
private $mailerService; //dependency as private property
//we're passing dependencies via constructor
public function __construct(\Swift_Mailer $mailerService)
{
$this->mailerService = $mailerService;
}
public function sendEmail($subject, $to, $body)
{
$msg = \Swift_Message::newInstance();
$msg->setSubject($subject);
$msg->setTo($to);
$msg->setBody($body);
$msg->setContentType('text/html');
$msg->setCharset('utf-8');
$msg->setFrom('test#gmail.com');
//now you can access mailer service like that
$this->mailerService->send($msg);
}
}
Now of course you need to modify the way you configure this service in Service Container.
You probably have something like this now:
services:
your_mailer:
class: TestBundle\UserBundle\Utilities\EmailServiceClass
Now you need to add arguments line in order to pass dependencies:
services:
your_mailer:
class: TestBundle\UserBundle\Utilities\EmailServiceClass
arguments: ['#mailer']
The last line defines arguments that will be passed to your service's constructor. mailer is the name of Swift_Mailer service.
More details about how to manage service dependencies can be found in Symfony's Book
Please don't extend the Controller class with a service class. You should inject the dependencies that you require using the services.yml. Please implement your service to be something along the lines of:
MyController.php:
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class MyController extends Controller
{
public function sendEmailAction()
{
$subject = //..
$to = //..
$body = //..
$this->get('email_service.class')->sendEmail($subject, $to, $body);
// Return a template, or redirect here..
return new Response();
}
}
EmailServiceClass.php
class EmailServiceClass
{
private $mailer;
public function __construct(\Swift_Mailer $mailer)
{
$this->mailer = $mailer;
}
public function sendEmail($subject, $to, $body)
{
$msg = \Swift_Message::newInstance();
$msg->setSubject($subject);
$msg->setTo($to);
$msg->setBody($body);
$msg->setContentType('text/html');
$msg->setCharset('utf-8');
$msg->setFrom('test#gmail.com');
$this->mailer->send($msg);
}
}
app/config/services.yml
email_service.class:
class: TestBundle\UserBundle\Utilities\EmailServiceClass
arguments: ['#mailer']