You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
During the unserialization of a User object (or any other Doctrine entity implementing Serializable) in an asynchronous email sending process using Mailer and Messenger, the user entity is not reloaded from the database. As a result, if you have a custom serialization/unserialization mechanism and the user object is a relation in another object, fields not covered by your custom unserialization process will be empty when rendering the email template in the asynchronous process.
Symfony stores the User object in the session data. If the object is large and the session data is stored in, for example, a database, you can minimize serialization (e.g., only serialize fields like email, password, roles, etc.). The remaining fields will always be reloaded from the database using the user provider.
The problem arises when we have another object, such as an Article, which has a relation to the User object, like an author field. If we use asynchronous email sending with Messenger, the default behavior is that the email template is rendered only in the asynchronous process. Before sending to Messenger, objects are serialized, and then, during the template rendering in the asynchronous process, they are unserialized. At this point, during the unserialization of such data, the user object is not fully reloaded, as it normally would be during typical Symfony operation.
How to reproduce
To illustrate the problem, let's assume we have 2 tables in the database: article and user, mapped to Doctrine entities: Article and User:
src/Entity/User.php
<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @ORM\Table(name="user")
*/
class User implements UserInterface, \Serializable
{
public function serialize()
{
return serialize([
$this->id,
$this->email,
$this->password,
]);
}
public function unserialize($serialized)
{
list(
$this->id,
$this->email,
$this->password,
) = unserialize($serialized);
}
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $email;
/**
* @ORM\Column(type="string", length=255)
*/
private $password;
/**
* @ORM\Column(type="string", length=255)
*/
private $slug; // <--- this field will not be unserialized
// ...
src/Entity/Article.php
<?php
// src/Entity/Article.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
* @ORM\Table(name="article")
*/
class Article
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $title;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User")
*/
private $author; // <-- User object will not be fully unserialized
// ...
src/Controller/EmailController.php
<?php
// src/Controller/EmailController.php
namespace App\Controller;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use App\Entity\Article;
class EmailController extends AbstractController
{
/**
* @Route("/send",
* name="send_email")
*
* @param Request $request
* @param EntityManagerInterface $em
* @param MailerInterface $mailer
* @return Response
*/
public function send(Request $request,
EntityManagerInterface $em,
MailerInterface $mailer): Response
{
$repository = $em->getRepository(Article::class);
$article = $repository->find(2);
// let's imagine that the retrieved Article object with ID = 2 has an associated User with ID = 1 and looks as follows:
//
// $author->id = 1;
// $author->email = "user@example.com";
// $author->password = "xxxxxx";
// $author->slug = "user-example-com";
//
// $article->id = 2;
// $article->title = "My article";
// $article->author = $author; // id = 1
$context = [];
$context['article'] = $article;
$address = $article->getAuthor()->getEmail(); // user@example.com
$email = (new TemplatedEmail())
->from("from@example.com")
->to($address)
->subject("New article")
->htmlTemplate("email/article.html.twig")
->context($context);
$mailer->send($email);
// at this point, the Article object is serialized and sent to the asynchronous process using Messenger.
return $this->render('index.html.twig');
}
}
templates/email/article.html.twig
This is your article:
{{ article.title }}
This is your email:
{{ article.author.email }}
This is your slug:
{{ article.author.slug }} {# <---- it will render an empty value #}
As a result, the email will be sent using the asynchronous process and the templates/email/article.html.twig template. However, unfortunately, the field {{ article.author.slug }} will be empty, even though the user object has the value user-example-com here.
Result (after rendering and sending the email):
This is your article:
My article
This is your email:
user@example.com
This is your slug:
^ it is empty where the slug should have been rendered.
The issue does not occur when the template is rendered before using the method MailerInterface::send.
src/Controller/EmailController.php
// src/Controller/EmailController.php
<?php
namespace App\Controller;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bridge\Twig\Mime\BodyRenderer;
use Twig\Environment;
use App\Entity\Article;
class EmailController extends AbstractController
{
/**
* @Route("/send",
* name="send_email")
*
* @param Request $request
* @param EntityManagerInterface $em
* @param MailerInterface $mailer
* @param Environment $twig
* @return Response
*/
public function send(Request $request,
EntityManagerInterface $em,
MailerInterface $mailer,
Environment $twig): Response
{
$repository = $em->getRepository(Article::class);
$article = $repository->find(2);
$context = [];
$context['article'] = $article;
$address = $article->getAuthor()->getEmail(); // user@example.com
$email = (new TemplatedEmail())
->from("from@example.com")
->to($address)
->subject("New article")
->htmlTemplate("email/article.html.twig")
->context($context);
// ---------------------
$renderer = new BodyRenderer($twig);
$renderer->render($email); // <--- render template before sending to Messenger
// ---------------------
$mailer->send($email);
// this time, the "slug" field of the User object will be correctly filled with "user-example-com".
return $this->render('index.html.twig');
}
}
The issue also does not occur during synchronous sending (without using Messenger).
It only occurs during asynchronous sending when using Messenger and when the template is rendered in the asynchronous process.
Possible Solution
If an object (even as a relation) that is to be used in rendering an email in the asynchronous Mailer process implements Serializable, it should be reloaded from the database beforehand, or any custom serialization should be ignored, and it should be fully serialized. This ensures the remaining fields are populated, just as they normally would be when the user object is reloaded from the database by Symfony's authentication mechanisms. Alternatively, the procedure for retrieving the full object could occur before sending it to Messenger, just before data serialization.
I'm not a Symfony member but I wouldn't consider this a bug. If you implement Serializable and use the default messenger serializer then it works exactly as expected. Remember the default serializer is just php serialize which is what that interface is for.
I feel that you have a few choices:
Remove the interface
Ensure all properties you want serialized are serialized
Use the json serializer instead so it doesn't use the Serializable interface
Ensure you only send arrays and scalars when using mailer async, and in custom messenger Messages.
If you really want doctrine to reload entities like that, you may need to work on a PR for that. I'm not aware of anything in Symfony that would do such a thing already.
Uh oh!
There was an error while loading. Please reload this page.
Symfony version(s) affected
5.4.42
Description
During the unserialization of a
User
object (or any other Doctrine entity implementingSerializable
) in an asynchronous email sending process usingMailer
andMessenger
, the user entity is not reloaded from the database. As a result, if you have a custom serialization/unserialization mechanism and the user object is a relation in another object, fields not covered by your custom unserialization process will be empty when rendering the email template in the asynchronous process.Symfony stores the
User
object in the session data. If the object is large and the session data is stored in, for example, a database, you can minimize serialization (e.g., only serialize fields like email, password, roles, etc.). The remaining fields will always be reloaded from the database using the user provider.The problem arises when we have another object, such as an
Article
, which has a relation to theUser
object, like an author field. If we use asynchronous email sending withMessenger
, the default behavior is that the email template is rendered only in the asynchronous process. Before sending toMessenger
, objects are serialized, and then, during the template rendering in the asynchronous process, they are unserialized. At this point, during the unserialization of such data, the user object is not fully reloaded, as it normally would be during typical Symfony operation.How to reproduce
To illustrate the problem, let's assume we have 2 tables in the database: article and user, mapped to
Doctrine
entities:Article
andUser
:src/Entity/User.php
src/Entity/Article.php
src/Controller/EmailController.php
templates/email/article.html.twig
As a result, the email will be sent using the asynchronous process and the
templates/email/article.html.twig
template. However, unfortunately, the field{{ article.author.slug }}
will be empty, even though the user object has the value user-example-com here.Result (after rendering and sending the email):
^ it is empty where the slug should have been rendered.
The issue does not occur when the template is rendered before using the method
MailerInterface::send
.src/Controller/EmailController.php
The issue also does not occur during synchronous sending (without using
Messenger
).It only occurs during asynchronous sending when using
Messenger
and when the template is rendered in the asynchronous process.Possible Solution
If an object (even as a relation) that is to be used in rendering an email in the asynchronous
Mailer
process implementsSerializable
, it should be reloaded from the database beforehand, or any custom serialization should be ignored, and it should be fully serialized. This ensures the remaining fields are populated, just as they normally would be when the user object is reloaded from the database by Symfony's authentication mechanisms. Alternatively, the procedure for retrieving the full object could occur before sending it toMessenger
, just before data serialization.Additional Context
Symfony's configuration:
.env
config/packages/mailer.yaml
config/packages/messenger.yaml
The text was updated successfully, but these errors were encountered: