Problem/Motivation
I tried to add a new TransportFactory and Transport class to handle a new DSN like foo://
This is possible with symfony mailer by following a few steps:
final class FooTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
return new FooTransport();
}
protected function getSupportedSchemes(): array
{
return ['foo'];
}
}
services:
mailer.transport_factory.foo:
class: App\FooTransportFactory
parent: mailer.transport_factory.abstract
tags:
- {name: mailer.transport_factory}
For now, the Drupal "MailerTransport" plugin allows us to define a new DSN, it is usable in the configuration, but that's all.
Symfony Mailer crash saying "Unknow Dsn scheme"
Is there a way to do it?
| Comment | File | Size | Author |
|---|---|---|---|
| #13 | symfony_mailer-3332398-13.patch | 6.29 KB | damien laguerre |
| #7 | symfony_mailer-3332398-6.patch | 9.05 KB | damien laguerre |
Issue fork symfony_mailer-3332398
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
damien laguerre commentedComment #3
adamps commentedThis function is entirely with the Symfony Mailer library so I suggest you ask there.
Comment #4
damien laguerre commentedThis is currently working with symfony.
The problem exist only with Drupal symfony mailer.
Comment #5
damien laguerre commentedHere are the tickets that concern the TransportFactory:
https://github.com/symfony/symfony/issues/31385
and
https://github.com/symfony/symfony/issues/35469
Comment #6
damien laguerre commentedThis is a POC.
I translated the Symfony Mailer services for Drupal.
This way, the ability to define a new transport factory is functional, as with Symfony.
Comment #7
damien laguerre commentedCancel and replace the previous patch... Sorry.
Comment #8
adamps commentedI don't really get this one yet😃.
1.
- $transport_dsn = $email->getTransportDsn();That can't be right surely? We need to use the provided DSN.
2 SymfonyMailerServiceProvider.php duplicates a lot of code with a long list of service providers. We don't want to maintain that code in this module.
Comment #9
damien laguerre commentedI applied the Symfony approach.
When you install symfony/mail on Symfony, Mailer services are declared on the project.
In Symfony, the default transport is defined in the .env file, and passed to the TransportFactory.
For this to work with Drupal, we can't use the database storage at the service build stage.
So I defined it in the settings.php.
It is not really a duplicate, it is normally generated by composing when symfony/mailer is installed on a Symfony project.
Using Symfony Mailer without declaring services, restricts the use of all Mailer possibilities.
Comment #10
adamps commentedI can't commit this unless it can be done without breaking things.
1.
$email->getTransportDsn() ;must be used.2.
Yes but this is not a symfony project it's a Drupal module. It seems very strange that symfony mailer library expects some other code to register services for it. Who says this is the right thing to do? Why doesn't it do so itself?
There is a list of factories in
Symfony\Component\Mailer\Transport::getDefaultFactories. We shouldn't duplicate that.Comment #11
damien laguerre commentedThere are some example of Symfony service define in the Drupal core.
Take a look at the file web/core/modules/serialization/serialization.services.yml
You will see some example of Symfony service declaration like:
In Symfony, services are defined by the framework-bundle package.
But when we use a Symfony package, we have to define it.
The mailer config file from the framework-bundle package :
https://github.com/symfony/framework-bundle/blob/6.2/Resources/config/ma...
This file is not included in the Mailer Package itself.
Comment #12
adamps commentedThanks for the explanation.
1. It's a good suggestion to make a service for
Symfony\Component\Mailer\Mailer. However currently this class only accepts a transport at create time - there is no way to change this later. This module allows each email to have a different transport. Therefore this isn't going to work, and we need to stick to creating a instance of the Mailer object for each message sent.You could raise a feature request for the library to add a way to specify a transport DSN per send request. This could be a new function or an extra parameter on the existing method.
The
Symfony\Component\Mailer\Transport\Transportshas the same problem - it requires an array of all possible transports at create time. This module cannot provide such a list, so we can't use this class as a service.However
Symfony\Component\Mailer\Transportdoesn't have this problem. It can be created with a list of factories. So this class could work as a service, and we can call thefromDsnObject()function. However this is messy, it requires registration of the service in code to add the list of factories as an argument.2. I stick to my belief that the details of all the transports don't belong in this module. Transports will get added, so it's chasing a moving target. Instead we need to call
Transport::getDefaultFactories().----
So putting everything together, I suggest the answer is like this.
1) Add code to the constructor of Mailer
- Copy the code currently in SymfonyMailerCompilerPass with comment "Retrieve all service transport factory tagged with mailer.transport_factory".
- Join this array with the result of calling
Transport::getDefaultFactories().- Create a
Transportobject using this combined array and store it in a protected class variable.2) Instead of calling
Transport::fromDsn()call$this->transport->fromDsnObject().No other changes needed.
There are no new services because each of the library 3 classes that could be a service are unsuitable, as they require information at create time that isn't easily available. This could change if symfony library code changes.
Comment #13
damien laguerre commentedThanks for the time you takes to analyze the problem and my code!
I have rewritten my patch with your suggestions.
The TransportFactoryManager class allows to register a new Factory in a service.yml with a tag.
I think it would be nice to keep the same registration system as describe in the Symfony Mailer documentation:
Comment #14
adamps commentedThanks, that looks good.
I agree.
I have some detailed comments. Please set the patch to needs review, check the tests pass and fix all coding standards.
1.
Comments need a description of the variable in addition to the @var.
2.
Do we actually need this function? Why not set
$this->transport =in the constructor?3. Let's put the call to
Transport::getDefaultFactories()intoSymfonyMailerCompilerPass. Instead of$transports = [];do$transports = Transport::getDefaultFactories(). Then the$factoriesconstructor parameter contains all the factories rather than just some of them, and we only callgetDefaultFactories()once.4.
Can we put this into
symfony_mailer.services.ymlas it's the more common place for people to look? Then we can have a comment to explain that an argument will be added.5.
Please implement an interface.
6.
We always use protected please. Also this needs a comment with a var statement.
7.
Inconsistent types - I suggest
TransportFactoryInterface[]is better, it is a stricter type. Also needs a description of the @param.8.
I prefer
@return TransportFactoryInterface[], also the needs a description beneath the @return.Comment #15
adamps commentedSorry I made a mistake in #12.
The "messy" code (SymfonyMailerCompilerPass) is necessary anyway to call
$container->findTaggedServiceIds(). So I propose we should create a service forTransportinstead of creatingTransportFactoryManager. Apologies for the extra work.Comment #17
damien laguerre commentedI made almost all the changes but I don't totally agree with everything.
4. Declaring the service in services.yml and completing it with a PassCompiler in another place is not ideal.
7. I had kept the same type as the package. I think it makes sense.
/*** @author Fabien Potencier
*/
final class Transports implements TransportInterface
{
private $transports;
private $default;
/**
* @param TransportInterface[] $transports
*/
public function __construct(iterable $transports)
Comment #18
damien laguerre commentedComment #19
adamps commentedGreat thanks. I have updated the MR with two changes:
Mailershould create the Transport object once during the constructor, not once per mail sent.service_collectortag corresponds totagged_iteratorin Symfony. Use this instead of creating a compiler pass.Please can you review and check it works in your system?
Comment #20
damien laguerre commentedGreat!
I first tried this way, but it could not be used with all the Symfony services I defined the first time.
Now it's really the best solution.
I did some tests and everything works fine!
Comment #22
adamps commentedGreat thanks