Problem/Motivation
Allow creating a Symfony Mailer EmailBuilder plugin / policy that is not hardwired to a module or config entity?
Proposed resolution
I propose a fix as follows:
- Add a label to the EmailBuilder annotation, which can be omitted if the plugin ID is an entity type or module name
- Add a boolean setting
proxyto the EmailBuilder annotation, which should be set for any builder proxied on behalf of another module - Fix
EmailBuilderManager::processDefinition(). The code to set$definition['provider']is only needed for proxy definitions. - Rename
EmailFactoryInterface::sendModuleEmail(), tosendTypedEmail(string $type).
Original issue:
I have a Drupal project with various custom emails, using the hook_mail() logic from Drupal core.
I have enabled symfony_mailer_bc.module and they seem to work.
However, I am trying to change my custom code to use a native Symfony Mailer implementation, so I can disable symfony_mailer_bc.module in the long run.
But I'm struggling to find good examples / documentation.
I my module I have 2 separate email flows (with different params), so I am creating 2 EmailBuilder plugins:
namespace Drupal\ipv_waiting_list\Plugin\EmailBuilder;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\symfony_mailer\EmailInterface;
use Drupal\symfony_mailer\MailerHelperTrait;
use Drupal\symfony_mailer\Processor\EmailBuilderBase;
use Drupal\user\UserInterface;
/**
* Defines the Email Builder plug-in for ipv_waiting_list module.
*
* @EmailBuilder(
* id = "ipv_waiting_list",
* sub_types = {
* "lesson_max_capacity" = @Translation("Email informing lesson reached maximum capacity - notification to coordinators and secretariat"),
* "participant" = @Translation("Email to participant on waiting list"),
* },
* common_adjusters = {"email_subject", "email_body", "email_skip_sending"},
* )
*/
class WaitingListEmailBuilder extends EmailBuilderBase {
use MailerHelperTrait;
/**
* Saves the parameters for a newly created email.
*
* @param \Drupal\symfony_mailer\EmailInterface $email
* The email to modify.
* @param \Drupal\user\UserInterface|null $user
* The (optional) mail receiving user.
* @param \Drupal\commerce_product\Entity\ProductVariationInterface|null $lesson
* The (optional) lesson object.
*/
public function createParams(EmailInterface $email, UserInterface $user = NULL, ProductVariationInterface $lesson = NULL) {
assert($user != NULL);
assert($lesson != NULL);
$email->setParam('user', $user);
$email->setParam('lesson', $lesson);
}
/**
* {@inheritdoc}
*/
public function build(EmailInterface $email) {
$email->setTo($email->getParam('user'));
}
}
and
namespace Drupal\ipv_waiting_list\Plugin\EmailBuilder;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\symfony_mailer\EmailInterface;
use Drupal\symfony_mailer\MailerHelperTrait;
use Drupal\symfony_mailer\Processor\EmailBuilderBase;
/**
* Defines the Email Builder plug-in for waiting list confirmation message.
*
* @EmailBuilder(
* id = "ipv_waiting_list_confirmation",
* sub_types = {
* "confirmation" = @Translation("Email confirming registration on waiting list"),
* },
* common_adjusters = {"email_subject", "email_body", "email_skip_sending"},
* )
*/
class WaitingListConfirmationEmailBuilder extends EmailBuilderBase {
use MailerHelperTrait;
/**
* Saves the parameters for a newly created email.
*
* @param \Drupal\symfony_mailer\EmailInterface $email
* The email to modify.
* @param string|null $recipient
* The (optional) mail receiving user.
* @param \Drupal\commerce_product\Entity\ProductVariationInterface|null $lesson
* The (optional) lesson.
*/
public function createParams(EmailInterface $email, string $recipient = NULL, ProductVariationInterface $lesson = NULL) {
assert($recipient != NULL);
$email->setParam('recipient', $recipient);
$email->setParam('lesson', $lesson);
}
/**
* {@inheritdoc}
*/
public function build(EmailInterface $email) {
$email->setTo($email->getParam('recipient'));
}
}
Now when I go to /admin/config/system/mailer/policy/add, I expect to see both in the Type dropdown, but I only see the module name. When I select that, I see the 2 subtypes of my WaitingListEmailBuilder class. No sign of the WaitingListConfirmationEmailBuilder type / subtype.
Is there only 1 EmailBuilder per module possible? Doesn't seem so, because symfony_mailer_bc contains multiple... Not sure why I'm not seeing them both though...
When I then select a subtype of WaitingListEmailBuilder and try to add a Body element, I get PHP notices and cannot proceed:
Notice: Undefined index: content in /app/web/modules/contrib/symfony_mailer/src/Plugin/EmailAdjuster/BodyEmailAdjuster.php on line 56
Notice: Trying to access array offset on value of type null in /app/web/modules/contrib/symfony_mailer/src/Plugin/EmailAdjuster/BodyEmailAdjuster.php on line 61
Also, I'm not sure what to replace the code with, that used to call the Drupal core mail.manager.
I now have:
/** @var \Drupal\symfony_mailer\EmailFactoryInterface $email_factory */
$email_factory = \Drupal::service('email_factory');
$email_factory->newModuleEmail('ipv_waiting_list_confirmation', 'confirmation')
->setParam('recipient', $entity->getEmail())
->setParam('commerce_product_variation', $this->lesson)
->send();
However I'm not sure if the module name or the type is needed for the first parameter. Seems the type should work, but why does this imply it's for a specific module? Maybe there should be a general $email_factory->newEmail()? Although that's probably nit-picking if I know I can use newModuleEmail().
It would seem to me this is a pretty basic use case? What am I doing wrong exactly? Am I missing some point?
I have checked the doc page https://www.drupal.org/docs/contributed-modules/symfony-mailer-0/develop... but that seems to be aimed towards letting my module work with symfony_mailer_bc, whereas I want it to work natively...
| Comment | File | Size | Author |
|---|---|---|---|
| #11 | symfony_mailer.builder-id.3281367-11.patch | 14.35 KB | adamps |
Issue fork symfony_mailer-3281367
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 #2
svendecabooterAfter some extra debugging, I found out that the plugin IDs should either be the same as a module name, or as a entity type.
That is not very clear from the documentation.
The second EmailBuilder I wanted to add is linked with a specific content entity, so I thought I'd create a plugin with the entity type as ID, and has_entity annotation set to TRUE.
However, the EmailFactory::newEntityEmail() only applies to config entities, not content entities.
So I guess I misinterpreted the purpose of that functionality again.
So I will change the purpose of this issue to the following:
Are there any plans to allow creating a simple Symfony Mailer EmailBuilder plugin / policy that is not hardwired to a module or config entity? Would that be complex to implement?
Comment #3
svendecabooterComment #4
svendecabooterComment #5
svendecabooterSome extra context about why I don't think all of my logic fits nicely in the module-specific Email Builder:
The module provides a simple form where a (anonymous) visitor enters their email address, to be added to the waiting list for a lesson (= product variation entity). They get a confirmation message about that, and a "waiting_list_entry" content entity gets created.
So here my EmailBuilder only receives the lesson parameter.
Afterwards an admin user can send an email message to a waiting list entry entity (like "Hey, a new spot is available").
This EmailBuilder needs to receive the waiting_list_entry entity parameter.
I guess I can create one plugin that receives one or the other entity, and switches logic for both use cases. But it feels like dirty code to me. Guess it'll be my only viable approach for now though. However I would have preferred a separate EmailBuilder and separate policy configuration for each type of mail.
Comment #6
adamps commentedThanks for a clear explanation.
I'd welcome a patch to improve it. It's fairly clear on EmailFactoryInterface, but I agree that's rather late, because you already wrote the EmailBuilder by then.
I have considered to allow the entity to be a content one. There might be some difficulties though. There can be 1000s of instances of a content entity which would lead to a corresponding huge number of different Mailer Policies. Content entities aren't required to have a string ID it can also be an int.
I agree, it would be great to have a doc page for creating a native email builder. I would be very happy if someone created it. There are currently two examples that I know of:
Did you try to create an EmailBuilder for a sub-type? If you look at
EmailFactory::initEmail()then there is some code in this direction: it searches for a plug-in name based on the "suggestions" - which means first TYPE_SUBTYPE then TYPE. Probably this code has never been tested. I would welcome patches that fix it.The email would still need to have a type and the function needs a name that clarifies it is an alternative to newEntityEmail(). So it could be
newTypedEmail()ornewStringEmail(). Which leads to your next question....The old hook_mail system required $module and $key and this module is similar. I see a difficulty with allowing use of any string for the type: how would you ensure the string is unique? True, your strings all starts with your module name, but even so it's not guaranteed unique as for example someone might create a contrib module called mail_send and someone else mail_send_bulk.
Comment #7
znerol commentedFiled #3281730: Individual email builder class for every subtype.
Comment #8
svendecabooter@znerol: ah yes thanks for spotting that! That does indeed allow me to define multiple plugins within the same module. Will update your issue with my additional findings.
Comment #9
adamps commentedI thought some more and I would like to fix this issue. It's the same for every plug-in - in theory there could be clash between plug-in ID in different modules but in practice it doesn't seem to be a problem.
I propose a fix as follows:
EmailBuilderManager::processDefinition(). The code to set$definition['provider']is only needed for back-compatibility emails, so should be moved intosymfony_mailer_bc_mailer_builder_info_alter, conditional on provider == symfony_mailer_bc.EmailFactoryInterface::sendModuleEmail(), my best idea issendTypedEmail(string $type).Comment #10
znerol commentedThis is a very good Idea.
Comment #11
adamps commentedPlease can people test and review?
Comment #13
adamps commentedComment #15
joelpittetIt may have helped us a bit track down a bug with our integration if the release notes for alpha11 had the interface changes listed out... maybe
Comment #17
mrweiner commentedCould somebody clarify what the annotations should look like when there are multiple EmailBuilders for a single module, and what the
sendTypedEmail()implementation should look like? I'm having trouble piecing it together from this issue and the documentation doesn't cover it as far as I can tell.EDIT: Nevermind -- there was a silent error in my createParams that was breaking things. For anybody who comes across this in the future, here are my two classes