Problem/Motivation
For #1803948: [META] Adopt the symfony mailer component, and specifically #3530860: Create a new mailer service to wrap symfony mailer, there should be a way to render HTML email inside an environment with partially changed system state / configuration. The current core mail manager only isolates the render context to prevent that cache metadata leaks into the current page. Research done by contrib suggests that this is not sufficient for HTML mails. The following things typically need to be changed in execution environment to make rendering safe and reproducible:
- Isolated render context (using Renderer::executeInRenderContext().
- Current user switched to anonymous user (using AccountSwitcher). Some contrib code also switches this to the email receiver (for messages with only one receiver).
- Active theme switched to the default theme (using ThemeManager::setActiveTheme. Some contrib code lets site administrators choose the mail theme.
- Default language switched depending on the message receiver. Core user hook_mail just activates the proper config override, contrib has various techniques to modify the language in other parts of the system.
Steps to reproduce
Proposed resolution
Introduce a simple but extensible Drupal\Core\Environment subsystem which permits to run code inside an environment where some of the system state and configuration is temporarily substituted.
In the context of #3530860: Create a new mailer service to wrap symfony mailer, this could be used in a Mailer class (or whatever we end up calling it) as follows:
$environmentParams = [
new EnvironmentParameter(RenderContextSubstitution::class),
new EnvironmentParameter(CurrentUserSubstitution::class, $email->getAccount()),
new EnvironmentParameter(ConfigOverrideLanguageSubstitution::class, $email->getLangcode()),
new EnvironmentParameter(ActiveThemeSubstitution::class, $email->getTheme()),
];
$this->environmentManager->executeInEnvironments(
$environmentParams,
fn () => $this->doSend($email),
);
);
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3536307
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
znerol commentedPushed a draft MR. With this mechanism it would be easy to just setup a default set of environment parameters, then let third parties mess with them before setting up and running code inside the patched environment.
Comment #4
adamps commentedThis is a subset of #3530860: Create a new mailer service to wrap symfony mailer.
Interesting. You have made it very generic and extensible. The downside is complexity: 3-4 times the lines of code compared with the straightforward approach in Drupal\symfony_mailer\Mailer and 9 extra files (plus it needs tests). The call stack becomes a harder to follow with many nested anonymous functions (whereas in fact only the render switch requires that - the others all operate inline).
My feeling is that we don't want such complexity unless we can conceive of a clear use case for it immediately inside Core. I do feel attracted to the idea of having an environment manager separate from the Mailer, and I would simplify it to a fixed set of things that it can alter, i.e. the 4 we already know. If anyone in Contrib really wants to add a 5th param then they can extend the class, and we could write our class to make that easy. Then we only need 2 files, probably less than half the code, and the interface simplifies to something like this (the manager will swap a NULL for the appropriate default)
OR perhaps we could keep
$environmentParamsif you prefer, with string keys['langcode' => 'XXX']and if they are missing it uses the default??When switching language, I believe we also need to do
FWIW I feel we don't want a separate event. We can use the same event as for altering any other parameter on the email. An extra event would be nuisance for code such as DSM+ Mailer Policy that supports altering any of the parameters generically.
Comment #5
znerol commentedComment #6
znerol commentedI moved the language switching to the
languagemodule. On monolingual sites this is now a no-op.Also found a use case for this in core: HelpTopicSection::renderTopicForSearch().
Comment #7
adamps commentedOK great. I'm not the right person to review the complexity of the implementation, so let's try and get another reviewer for that.
This does seem to meet the needs of the mailer project. I have put in an example in the IS that shows it fits in the context of #3530860: Create a new mailer service to wrap symfony mailer. Of course this is based on just one possible idea of solving that issue and the details would change, but the principle seems good anyway. Equally it would fit in
MailManager(not that I propose changing that now!) or DSM+.Comment #9
webflo commentedI have discussed this problem space during Drupal Dev Days with @znerol
I have tried to send mails in different languages in a single request (or later in symfony messenger).
I found various issues during this process.
LanguageRequestSubscriber::onContainerInitializeSubrequestFinished handles config overrides, but a few other services in Drupal core need a manual reset because they are not language aware and have already been initialized.
The issue are in TypedDataManager, EntityFieldManager, EntityMemoryCache and options_allowed_values().
I have a class called MailLanguageManager which handles the reset in combination with a custom language negotiation plugin.
The complete code (MailLanguageManager and LanguageNegotiationMail) is this github gist: https://gist.github.com/webflo/af03a8d49cd546ead4903669fe9dadf8
Comment #10
znerol commentedOptions are being refactored in #2171397: Deprecate remaining options.module functions.
Comment #11
znerol commentedThe field label stuff is quite a rabbit hole, good entry point is this #3221375: Wrong language field labels after `drush cr` because of Drush language negotiation
Comment #12
znerol commentedBase field definitions should be covered by #3114824: Add multilingual support for caching basefield definitions.