Skip to content

[Mailer, Messenger] User object is not unserialized correctly in asynchronous process #58685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
szczyglis-dev opened this issue Oct 27, 2024 · 3 comments

Comments

@szczyglis-dev
Copy link

szczyglis-dev commented Oct 27, 2024

Symfony version(s) affected

5.4.42

Description

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.

Additional Context

Symfony's configuration:

.env

# .env

MAILER_DSN=smtp://user:password@smtp.example.com
MESSENGER_TRANSPORT_DSN=amqp://guest:guest@127.0.0.1:5672/%2f/mailer

config/packages/mailer.yaml

# config/packages/mailer.yaml

framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'
        envelope:
            sender: 'sender@example.com'

config/packages/messenger.yaml

# config/packages/messenger.yaml

framework:
    messenger:
        transports:
            mailer: '%env(MESSENGER_TRANSPORT_DSN)%'
        routing:
            'Symfony\Component\Mailer\Messenger\SendEmailMessage': mailer
@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Friendly reminder that this issue exists. If I don't hear anything I'll close this.

@nesl247
Copy link

nesl247 commented May 16, 2025

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:

  1. Remove the interface
  2. Ensure all properties you want serialized are serialized
  3. Use the json serializer instead so it doesn't use the Serializable interface
  4. 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.

@carsonbot carsonbot removed the Stalled label May 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants
@nesl247 @carsonbot @szczyglis-dev and others
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy