MailFactory is now a swappable service thanks to #1889644: Convert drupal_mail_system() to a MailFactory service to allow more flexible replacements. However, alternative implementations are hobbled by the lack of any kind of discovery mechanism for MailInterface classes.
For example, a contrib module like Mailsystem has no way of building a user interface for selecting mail methods, because it has no way of knowing what mail classes are available.
A simple and low-impact solution would be turn existing mail classes into annotated plugins and create a simple plugin manager for them. This would open up options for contrib.
Proposed change notice
Mail backends are now annotated plugins, and can be discovered and implemented via the new plugin.manager.mail service.
The earlier mail.factory service has been removed, and its functionality has been merged into plugin.manager.mail.
Furthermore, the config setting system.mail.interface, which identifies the mail plugin used for a particular message, now takes plugin IDs as values rather than class names.
Creating a mail plugin
To provide a new mail backend in a custom module named mymodule, you must implement Drupal\Core\Mail\MailInterface within the \Drupal\mymodule\Plugin\Mail namespace, and annotate the class:
namespace Drupal\mymodule\Plugin\Mail;
use Drupal\Core\Mail\MailInterface;
/**
* A custom mail backend.
*
* @Mail(
* id = "mymodule_mailer",
* label = @Translation("My custom mailer"),
* description = @Translation("An example mail plugin implementation.")
* )
*/
class MymoduleMailer implements MailInterface {
public function format(array $message) {
// ...
}
public function mail(array $message) {
// ...
}
}
See the API documentation for MailInterface for implementation details.
Retrieving mail plugins
Previously in Drupal 8, there was no way to retrieve a list of MailInterface classes. Now, you can use the plugin.manager.mail service:
// Get a keyed array of mail plugin definitions.
$definitions = \Drupal::service('plugin.manager.mail')->getDefinitions();
// Get a single definition by plugin ID.
$plugin_id = 'php_mail';
$definition = \Drupal::service('plugin.manager.mail')->getDefinition($plugin_id);
// Create an instance of a specific mail plugin.
$plugin_id = 'php_mail';
$mailer = \Drupal::service('plugin.manager.mail')->createInstance($plugin_id);
Mail plugin selection
The Drupal 8 config setting system.mail.interface is a keyed array that maps the message ID to the mail plugin it should use. The values used in this array are now plugin IDs.
Before in Drupal 8
\Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\Plugin\Mail\PhpMail')->save();
After
\Drupal::config('system.mail')->set('interface.default', 'php_mail')->save();
| Comment | File | Size | Author |
|---|---|---|---|
| #71 | 2187495-mail-plugins-71.patch | 32.37 KB | longwave |
Comments
Comment #1
les limHere's a first pass at it. There will probably be tests that need refactoring.
Comment #3
les limFixed the mail implementation in WebTestBase, which should account for most of the failures. The tests in MailTest will need some deeper renovation, though.
Comment #4
les limDang it, test this one, not that one.
Comment #5
les limComment #7
les limThis one should be green.
Comment #10
berdirAgreed that this would be great to have but it would be good to have this approved as an API change before you waste time on it if it won't be possible to get it in. Tagging as API change and beta target, as it probably has to happen before beta.
components can provide plugins too, it just work just fine if you have it in Drupal\Core\Mail\Plugin\Mail\PhpMail.
We started experimenting with a plugin manager and a more flexible mail factory for mailsystem as well, as we want to improve the ability to use separate plugins for send and format there. We just added the core implementation as a hardcoded list. Wondering what's the best way to combine them.
Previously, mailsystem used to find implementations by searching the registry table for a given class suffix, similar to how simpletest works in 7.x, that is now impossible, so we need some kind of discovery.
Comment #11
alexpottThis change looks like a good idea. In fact because of
you could argue that this is a regression. If @Berdir and @Les Lim can agree a scope and a way forward that is as limited as possible and with the least impact of other parts of Drupal I'm +1 to doing this.
Comment #12
les limThanks, Berdir and alexpott!
Well, MailManager and MailFactory can be easily combined, since MailFactory only contains a public get() method. That can just be moved into MailManager.
With regard to @alexpott's stipulations, though, I don't know if there's a low-impact way to have core use different plugins for format and for send. That may be okay, though; as long as they're plugins, contrib can find a way forward for this.
Comment #13
les limChanges since last patch:
Comment #14
berdirI think you can leave out the "for development", this is really only about testing.
When changing Overrides/Implements docblock, just change them to {@inheritdoc}
Looks great.
I think we will want to update/expand the existing change record at https://drupal.org/node/2073215. You could already prepare an updated version of that in the issue summary here, the new change record draft system can't handle new revisions yet I think :)
Comment #15
les limDone.
Since the purpose of the override is to prevent sending the message, the parent docblock doesn't apply. I've better documented the docblock to explain this.
Also removed the annotation-specific "use" statements -- I followed some obsolete documentation.
Comment #17
les limWhoopsies.
Comment #18
tim.plunkettI think this should use getInstance, which is on MapperInterface (which all plugin managers implement).
See FormatterPluginManager, WidgetPluginManager, SelectionPluginManager, or ResourcePluginManager for example implementations
I can't remember offhand, but we usually use a more specific exception class for this
Use is_subclass_of
$config_factory
There's no written rule for this, but every other plugin ID in core is lower_snake_case, not UpperCamelCase.
Comment #19
les limComment #20
les limUpdated patch with the fixes from #18.
Comment #21
les limUpdating change notice proposal for lower_snake_case plugin IDs.
Comment #22
les limComment #23
les limSomehow missed updating the docblock for
drupal_mail_system().Comment #24
berdirFirst you change object to instance but below you keep it? Should probably use instance in both cases?
Our coding standards have nothing to say about this, so I'm not sure. There are issues to discuss this, so let's not waste time on this :)
Looks like git doesn't see this file as a move anymore, maybe you can use lower -M numbers (e.g. git diff -M25) to force it, would make the changes easier to review. Don't worry if it doesn't work, it's a small class anyway.
Looks good otherwise I think, change notice also looks great.
Comment #26
les limGood point. Here's an updated patch for more standardized language in docblocks. Also used git diff -M25, which even picked up the move from MailFactory to MailManager.
Comment #27
berdirThe idea to use the same class not only for the test but also an actual test implementation is so crazy, yay for cleaning that up :)
This looks awesome, and now the diff is very easy to understand and shows what is changing, great. This is RTBC I think :)
Also tagging as approved API change, based on alexpott's comment above.
Comment #28
alexpottcore-2187495-26-mail-to-plugins.patch no longer applies.
Comment #29
les limRerolled.
Comment #31
longwave29: core-2187495-29-mail-to-plugins.patch queued for re-testing.
Comment #32
les limBack to RTBC.
Comment #33
alexpottLets create a change record before commit https://groups.drupal.org/node/402688
Comment #34
alexpottComment #35
les limThere's one ready to go in the issue summary. Pasted that into a new draft change record here:
https://drupal.org/node/2191201
Comment #36
les limAfter committing, I'll also update the earlier change record from when MailFactory was introduced. That one is here: https://drupal.org/node/2073215
Comment #37
les limRerolled since #2171683: Remove all Simpletest overrides and rely on native multi-site functionality instead got in.
Comment #38
les limResetting status.
Comment #39
longwaveWhile someone's looking at the mail system could they review #2146971: SimpleTest should override all mail system implementations with TestMailCollector? It will need rerolling after this gets in, should be simple enough though.
Comment #40
alexpottCan we implement an @Mail annotation. See https://api.drupal.org/api/drupal/core%21modules%21block%21lib%21Drupal%...
Comment #41
longwaveComment #42
les limThanks! Updated the change notice draft at https://drupal.org/node/2191201 as well.
Comment #43
tim.plunkettI'm not
I'm not sure how this passes, all of the other managers using custom annotations have to specify it here.
Also, why are we not providing an alter hook? The rest of the managers do.
Comment #44
les limFixes for #43. Setting back for review.
Comment #45
les limRe-rolled #44.
Comment #46
alexpottConsidering https://drupal.org/node/2187495#comment-8471327 did not fail we must be missing test coverage. No? Or put anyway way how did it pass?
Comment #47
alexpottFrom an IRC conversation with @Les Lim
Comment #48
longwaveThe mail plugins define a label and description, but nothing uses them, and there is no test coverage for them - maybe that would have picked up the incorrect annotation config?
Comment #49
berdirI don't think so, you can put anything on an annotation, the classes are just for documentation.
That said, it would probably be useful to have some explicit test coverage of the plugin manager, and get the definitions and verify the label in there?
Comment #50
les limRe: #49, there's already test coverage for getting definitions from DefaultPluginManager in
\Drupal\Core\Plugin\DefaultPluginManagerTest->testDefaultPluginManager().Do you mean we should test this again for MailManager specifically? Seems unnecessary to me.
Comment #51
tim.plunkettOverriding getInstance() is not very common, and at least having a unit test for that would be useful.
Comment #52
berdirWhat I was suggesting wouldn't be a unit test as we currently can not unit test annotation based plugin discovery due to @Translation which calls t(). But yes, unit tests for the getInstance() would possibly be useful.
Comment #53
les limAdded unit tests -- thanks to Berdir and tim.plunkett for the IRC advice.
Comment #54
sunPlease note that we have two plugin concepts baked into our current Mail API right now:
Mail formatting
The gist of
format(). Typically implemented by Mimemail module and others, which only care about formatting (and headers), but do NOT care for how the mail is sent in the end.Mail sending
The gist of
send(). Typically implemented by SMTP module and PHPMailer module, which only (or mostly) care about mail sending, but do not care for how the mail is formatted (as long as mail headers are reflecting the chosen encoding).I originally assumed that a proper separation of the two concepts was still an unresolved issue for D8, but after grepping core issues, I was only able to find #331180: fix pluggable smtp/mail framework, which is fixed since D7 already.
In any case, a plugin-ification of the mail system should take this separation of concerns into account. — That is, because there is hardly a use-case for a mail plugin in the wild that implements both. - Above mentioned (prime) examples are just a testament to that.
Unless I missed something, the clean separation should exist in core already. It is merely possible that the default PhpMail implementation happens to be a provider for both plugin types, because Drupal core does not support any other format than plain-text. However, that coincidence does not and should not mean that a separation of concerns was not foreseen. :-)
Comment #56
les limIt is still unresolved; you might be thinking of #1135262: drupal_mail() should support using different MailSystemInterface classes for format() and mail(). In any case, D8 has MailInterface, which is pretty much identical to D7's MailSystemInterface, both of which mandate implementing both format() and mail() methods in your mail backend.
I would love love looove it if we could do what you're saying, and break this into MailFormatterInterface and MailDeliveryInterface or some such. I'd be more than willing to own the development of that. This is just the patch I thought had a shot of going in, and it does at least unblock contrib from being better able to work around these constraints.
Comment #57
les limFixes tests.
Comment #58
les limTweaked #57 to mock LanguageManagerInterface instead of LanguageManager. (HT tim.plunkett)
Comment #59
berdirI would really prefer to not complicate this issue further by splitting that up, as much as I agree it would be nicer.
This is already so much better than what we had in 7.x, we already have an initial 8.x port of mailsystem that extends the MailManager and uses an adapter class to make it possible to select two different classes for format() and send(). No longer required to generate code for that as in 7.x :)
Comment #60
andypostNo reason to make split here. Better to file follow-up for formatting questions, including pointed in #1856562-11: Convert "Subject" and "Message" into Message base fields (2,3)
I think description is useless, This should be placed in class description.
Also "name" could be more informative
Contains
@inheritdoc
Comment #61
berdir1. Description isn't useless when the plugins are exposed in the UI, e.g. in mailsystem.
3. This adds additional comments. Per our current coding standards, it is not allowed to use @inheritdoc there. That said, it would IMHO be perfectly fine to keep the inline comment there instead. I would say that we don't override the parent class but implement the method defined in the interface and @inheritdoc points to that. Then the inline comment explains that our implementation is simply "always fail".
Comment #62
longwaveThis patch improves some of the comments and labelling. I agree that "description" may be useful in contrib.
Comment #63
longwaveWrong interdiff.
Comment #64
berdir@longwave: Those two fails are identical?
Pro-tip: Do not name your interdiff's interdiff.txt. As you can see, they get renamed to interdiff_xxxx.txt, and the way drupal.org figures out what number to use, it has to loop from 1 to (currently) 2386, checking if the file already exists on every loop. That's not nice.
I assume you copied that from the entity manager test, but we have a default plugin manager here, so..
a) We should just use DiscoveryInterface, not Cached.
b) getDefinition() on the discovery should *not* be called, so remove that.
Super-minor: module/key seems a bit strange, can we use something like example_testkey instead?
Comment #65
les limUpdated for #64.
Comment #66
berdirThanks, this looks good I think!
Comment #67
longwaveSo isn't this a bug in Drupal/drupal.org that we should consider fixing? :)
Comment #68
les limNeeded reroll.
Comment #69
les limComment #70
alexpottWas about to commit...
:(
Comment #71
longwavecore/modules/system/lib/Drupal/system/Tests/InstallerTest.php => core/modules/simpletest/lib/Drupal/simpletest/InstallerTestBase.php
Comment #73
alexpott71: 2187495-mail-plugins-71.patch queued for re-testing.
Comment #74
berdirThanks for the re-roll, back to RTBC then.
Comment #75
alexpottCommitted 438494d and pushed to 8.x. Thanks!