+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index 7bd8659913..89534ea5b6 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -32,6 +32,7 @@
+
@@ -539,4 +540,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php
new file mode 100644
index 0000000000..ef8cdd385c
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php
@@ -0,0 +1,11 @@
+loadFromExtension('framework', [
+ 'mailer' => [
+ 'dsn' => 'smtp://example.com',
+ 'envelope' => [
+ 'sender' => 'sender@example.org',
+ 'recipients' => ['redirected@example.org', 'redirected1@example.org'],
+ ],
+ ],
+]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer.xml
new file mode 100644
index 0000000000..ff4d75c825
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ sender@example.org
+ redirected@example.org
+ redirected1@example.org
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer.yml
new file mode 100644
index 0000000000..07d435d9df
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer.yml
@@ -0,0 +1,8 @@
+framework:
+ mailer:
+ dsn: 'smtp://example.com'
+ envelope:
+ sender: sender@example.org
+ recipients:
+ - redirected@example.org
+ - redirected1@example.org
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
index b118de7334..5fb865ee1d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
@@ -1378,6 +1378,19 @@ abstract class FrameworkExtensionTest extends TestCase
], $defaultOptions['peer_fingerprint']);
}
+ public function testMailer(): void
+ {
+ $container = $this->createContainerFromFile('mailer');
+
+ $this->assertTrue($container->hasAlias('mailer'));
+ $this->assertTrue($container->hasDefinition('mailer.default_transport'));
+ $this->assertSame('smtp://example.com', $container->getDefinition('mailer.default_transport')->getArgument(0));
+ $this->assertTrue($container->hasDefinition('mailer.envelope_listener'));
+ $l = $container->getDefinition('mailer.envelope_listener');
+ $this->assertSame('sender@example.org', $l->getArgument(0));
+ $this->assertSame(['redirected@example.org', 'redirected1@example.org'], $l->getArgument(1));
+ }
+
protected function createContainer(array $data = [])
{
return new ContainerBuilder(new ParameterBag(array_merge([
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index db1d38b710..a315cde7f4 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -72,6 +72,7 @@
"symfony/dotenv": "<4.4",
"symfony/dom-crawler": "<4.4",
"symfony/form": "<4.4",
+ "symfony/lock": "<4.4",
"symfony/messenger": "<4.4",
"symfony/property-info": "<4.4",
"symfony/serializer": "<4.4",
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Router/panel.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Router/panel.html.twig
index ea8600a2d0..41636d1440 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Router/panel.html.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Router/panel.html.twig
@@ -5,13 +5,6 @@
{{ request.route ?: '(none)' }}
Matched route
-
- {% if request.route %}
-
- {{ traces|length }}
- Tested routes before match
-
- {% endif %}
{% if request.route %}
diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php
index ff9a8d72f9..216408cce3 100644
--- a/src/Symfony/Component/Debug/DebugClassLoader.php
+++ b/src/Symfony/Component/Debug/DebugClassLoader.php
@@ -153,11 +153,11 @@ class DebugClassLoader
if (!$file = $this->classLoader[0]->findFile($class) ?: false) {
// no-op
} elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) {
- require $file;
+ include $file;
return;
} else {
- require $file;
+ include $file;
}
} else {
($this->classLoader)($class);
diff --git a/src/Symfony/Component/HttpClient/Psr18Client.php b/src/Symfony/Component/HttpClient/Psr18Client.php
index 9d3628afb0..ee8c813b46 100644
--- a/src/Symfony/Component/HttpClient/Psr18Client.php
+++ b/src/Symfony/Component/HttpClient/Psr18Client.php
@@ -92,7 +92,7 @@ final class Psr18Client implements ClientInterface, RequestFactoryInterface, Str
}
}
- $body = isset(class_uses($response)[ResponseTrait::class]) ? $response->toStream() : StreamWrapper::createResource($response, $this->client);
+ $body = isset(class_uses($response)[ResponseTrait::class]) ? $response->toStream(false) : StreamWrapper::createResource($response, $this->client);
return $psrResponse->withBody($this->streamFactory->createStreamFromResource($body));
} catch (TransportExceptionInterface $e) {
diff --git a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php
index 10c2d505c3..ed7281e1be 100644
--- a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php
+++ b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php
@@ -21,6 +21,10 @@ use Symfony\Component\HttpClient\Exception\RedirectionException;
use Symfony\Component\HttpClient\Exception\ServerException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\ClientState;
+use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* Implements the common logic for response classes.
@@ -182,11 +186,16 @@ trait ResponseTrait
* Casts the response to a PHP stream resource.
*
* @return resource|null
+ *
+ * @throws TransportExceptionInterface When a network error occurs
+ * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
+ * @throws ClientExceptionInterface On a 4xx when $throw is true
+ * @throws ServerExceptionInterface On a 5xx when $throw is true
*/
- public function toStream()
+ public function toStream(bool $throw = true)
{
// Ensure headers arrived
- $this->getStatusCode();
+ $this->getHeaders($throw);
return StreamWrapper::createResource($this, null, $this->content, $this->handle && 'stream' === get_resource_type($this->handle) ? $this->handle : null);
}
diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md
index fc4bc0fc9c..65421f31e8 100644
--- a/src/Symfony/Component/HttpKernel/CHANGELOG.md
+++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md
@@ -24,6 +24,13 @@ CHANGELOG
* removed methods `serialize` and `unserialize` from `DataCollector`, store the serialized state in the data property instead
* made `ProfilerStorageInterface` internal
+4.4.0
+-----
+
+ * Implementing the `BundleInterface` without implementing the `getPublicDir()` method is deprecated.
+ This method will be added to the interface in 5.0.
+ * The `DebugHandlersListener` class has been marked as `final`
+
4.3.0
-----
diff --git a/src/Symfony/Component/Lock/Store/CombinedStore.php b/src/Symfony/Component/Lock/Store/CombinedStore.php
index f2a2c4dc57..b5891af830 100644
--- a/src/Symfony/Component/Lock/Store/CombinedStore.php
+++ b/src/Symfony/Component/Lock/Store/CombinedStore.php
@@ -98,7 +98,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
*/
public function waitAndSave(Key $key)
{
- @trigger_error(sprintf('%s::%s has been deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', \get_class($this), __METHOD__), E_USER_DEPRECATED);
+ @trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', \get_class($this)));
}
diff --git a/src/Symfony/Component/Lock/Store/MemcachedStore.php b/src/Symfony/Component/Lock/Store/MemcachedStore.php
index 67b7d8eefb..962f143c76 100644
--- a/src/Symfony/Component/Lock/Store/MemcachedStore.php
+++ b/src/Symfony/Component/Lock/Store/MemcachedStore.php
@@ -74,7 +74,7 @@ class MemcachedStore implements StoreInterface
*/
public function waitAndSave(Key $key)
{
- @trigger_error(sprintf('%s has been deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__));
+ @trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__));
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', \get_class($this)));
}
diff --git a/src/Symfony/Component/Lock/Store/PdoStore.php b/src/Symfony/Component/Lock/Store/PdoStore.php
index 70e041f292..dc97630810 100644
--- a/src/Symfony/Component/Lock/Store/PdoStore.php
+++ b/src/Symfony/Component/Lock/Store/PdoStore.php
@@ -145,7 +145,7 @@ class PdoStore implements StoreInterface
*/
public function waitAndSave(Key $key)
{
- @trigger_error(sprintf('%s has been deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__));
+ @trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__));
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', __METHOD__));
}
diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php
index c0ae6ea9b3..4ac41ac347 100644
--- a/src/Symfony/Component/Lock/Store/RedisStore.php
+++ b/src/Symfony/Component/Lock/Store/RedisStore.php
@@ -77,7 +77,7 @@ class RedisStore implements StoreInterface
*/
public function waitAndSave(Key $key)
{
- @trigger_error(sprintf('%s::%s has been deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', \get_class($this), __METHOD__), E_USER_DEPRECATED);
+ @trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', \get_class($this)));
}
diff --git a/src/Symfony/Component/Lock/Store/ZookeeperStore.php b/src/Symfony/Component/Lock/Store/ZookeeperStore.php
index 0de0a372f7..d089590f88 100644
--- a/src/Symfony/Component/Lock/Store/ZookeeperStore.php
+++ b/src/Symfony/Component/Lock/Store/ZookeeperStore.php
@@ -87,7 +87,7 @@ class ZookeeperStore implements StoreInterface
*/
public function waitAndSave(Key $key)
{
- @trigger_error(sprintf('%s::%s has been deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', \get_class($this), __METHOD__), E_USER_DEPRECATED);
+ @trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException();
}
diff --git a/src/Symfony/Component/Lock/Tests/LockTest.php b/src/Symfony/Component/Lock/Tests/LockTest.php
index 3fddbc3eca..9faf05d12c 100644
--- a/src/Symfony/Component/Lock/Tests/LockTest.php
+++ b/src/Symfony/Component/Lock/Tests/LockTest.php
@@ -18,7 +18,6 @@ use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\PersistStoreInterface;
-use Symfony\Component\Lock\StoreInterface;
/**
* @author Jérémy Derussé
@@ -41,7 +40,7 @@ class LockTest extends TestCase
public function testAcquireNoBlockingStoreInterface()
{
$key = new Key(uniqid(__METHOD__, true));
- $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$lock = new Lock($key, $store);
$store
@@ -59,7 +58,7 @@ class LockTest extends TestCase
public function testPassingOldStoreInterface()
{
$key = new Key(uniqid(__METHOD__, true));
- $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$lock = new Lock($key, $store);
$store
@@ -86,7 +85,7 @@ class LockTest extends TestCase
public function testAcquireReturnsFalseStoreInterface()
{
$key = new Key(uniqid(__METHOD__, true));
- $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$lock = new Lock($key, $store);
$store
@@ -121,7 +120,7 @@ class LockTest extends TestCase
public function testAcquireSetsTtl()
{
$key = new Key(uniqid(__METHOD__, true));
- $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
@@ -138,7 +137,7 @@ class LockTest extends TestCase
public function testRefresh()
{
$key = new Key(uniqid(__METHOD__, true));
- $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
@@ -152,7 +151,7 @@ class LockTest extends TestCase
public function testRefreshCustom()
{
$key = new Key(uniqid(__METHOD__, true));
- $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
@@ -201,7 +200,7 @@ class LockTest extends TestCase
public function testReleaseStoreInterface()
{
$key = new Key(uniqid(__METHOD__, true));
- $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
@@ -356,7 +355,7 @@ class LockTest extends TestCase
public function testExpirationStoreInterface($ttls, $expected)
{
$key = new Key(uniqid(__METHOD__, true));
- $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
foreach ($ttls as $ttl) {
diff --git a/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php
index 2ab030b200..4039d8fd95 100644
--- a/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php
+++ b/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php
@@ -14,7 +14,7 @@ namespace Symfony\Component\Lock\Tests\Store;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
-use Symfony\Component\Lock\StoreInterface;
+use Symfony\Component\Lock\PersistStoreInterface;
/**
* @author Jérémy Derussé
@@ -22,7 +22,7 @@ use Symfony\Component\Lock\StoreInterface;
abstract class AbstractStoreTest extends TestCase
{
/**
- * @return StoreInterface
+ * @return PersistStoreInterface
*/
abstract protected function getStore();
diff --git a/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php b/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php
index a22224215c..1ac99980b5 100644
--- a/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php
+++ b/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php
@@ -14,7 +14,7 @@ namespace Symfony\Component\Lock\Tests\Store;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\NotSupportedException;
use Symfony\Component\Lock\Key;
-use Symfony\Component\Lock\StoreInterface;
+use Symfony\Component\Lock\PersistStoreInterface;
/**
* @author Jérémy Derussé
@@ -24,7 +24,7 @@ trait BlockingStoreTestTrait
/**
* @see AbstractStoreTest::getStore()
*
- * @return StoreInterface
+ * @return PersistStoreInterface
*/
abstract protected function getStore();
diff --git a/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php
index 9c38f97ece..3292247fde 100644
--- a/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php
+++ b/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php
@@ -17,7 +17,6 @@ use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\PersistStoreInterface;
use Symfony\Component\Lock\Store\CombinedStore;
use Symfony\Component\Lock\Store\RedisStore;
-use Symfony\Component\Lock\StoreInterface;
use Symfony\Component\Lock\Strategy\StrategyInterface;
use Symfony\Component\Lock\Strategy\UnanimousStrategy;
@@ -268,8 +267,8 @@ class CombinedStoreTest extends AbstractStoreTest
public function testPutOffExpirationIgnoreNonExpiringStorage()
{
- $store1 = $this->getMockBuilder(StoreInterface::class)->getMock();
- $store2 = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store1 = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
+ $store2 = $this->getMockBuilder(PersistStoreInterface::class)->getMock();
$store = new CombinedStore([$store1, $store2], $this->strategy);
diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Factory/SesTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Factory/SesTransportFactory.php
new file mode 100644
index 0000000000..ca6fd49829
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Factory/SesTransportFactory.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Amazon\Factory;
+
+use Symfony\Component\Mailer\Bridge\Amazon;
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class SesTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ $scheme = $dsn->getScheme();
+ $user = $this->getUser($dsn);
+ $password = $this->getPassword($dsn);
+ $region = $dsn->getOption('region');
+
+ if ('api' === $scheme) {
+ return new Amazon\Http\Api\SesTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger);
+ }
+
+ if ('http' === $scheme) {
+ return new Amazon\Http\SesTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger);
+ }
+
+ if ('smtp' === $scheme) {
+ return new Amazon\Smtp\SesTransport($user, $password, $region, $this->dispatcher, $this->logger);
+ }
+
+ throw new UnsupportedSchemeException($dsn);
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'ses' === $dsn->getHost();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Factory/SesTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Factory/SesTransportFactoryTest.php
new file mode 100644
index 0000000000..595f725828
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Factory/SesTransportFactoryTest.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Amazon\Tests\Factory;
+
+use Symfony\Component\Mailer\Bridge\Amazon;
+use Symfony\Component\Mailer\Bridge\Amazon\Factory\SesTransportFactory;
+use Symfony\Component\Mailer\Tests\TransportFactoryTestCase;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
+
+class SesTransportFactoryTest extends TransportFactoryTestCase
+{
+ public function getFactory(): TransportFactoryInterface
+ {
+ return new SesTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('api', 'ses'),
+ true,
+ ];
+
+ yield [
+ new Dsn('http', 'ses'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'ses'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ $client = $this->getClient();
+ $dispatcher = $this->getDispatcher();
+ $logger = $this->getLogger();
+
+ yield [
+ new Dsn('api', 'ses', self::USER, self::PASSWORD),
+ new Amazon\Http\Api\SesTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('api', 'ses', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
+ new Amazon\Http\Api\SesTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('http', 'ses', self::USER, self::PASSWORD),
+ new Amazon\Http\SesTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('http', 'ses', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
+ new Amazon\Http\SesTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('smtp', 'ses', self::USER, self::PASSWORD),
+ new Amazon\Smtp\SesTransport(self::USER, self::PASSWORD, null, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('smtp', 'ses', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
+ new Amazon\Smtp\SesTransport(self::USER, self::PASSWORD, 'eu-west-1', $dispatcher, $logger),
+ ];
+ }
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ yield [new Dsn('foo', 'ses', self::USER, self::PASSWORD)];
+ }
+
+ public function incompleteDsnProvider(): iterable
+ {
+ yield [new Dsn('smtp', 'ses', self::USER)];
+
+ yield [new Dsn('smtp', 'ses', null, self::PASSWORD)];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Google/Factory/GmailTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Google/Factory/GmailTransportFactory.php
new file mode 100644
index 0000000000..d96a471018
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Google/Factory/GmailTransportFactory.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Google\Factory;
+
+use Symfony\Component\Mailer\Bridge\Google\Smtp\GmailTransport;
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class GmailTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ if ('smtp' === $dsn->getScheme()) {
+ return new GmailTransport($this->getUser($dsn), $this->getPassword($dsn), $this->dispatcher, $this->logger);
+ }
+
+ throw new UnsupportedSchemeException($dsn);
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'gmail' === $dsn->getHost();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Google/Tests/Factory/GmailTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Google/Tests/Factory/GmailTransportFactoryTest.php
new file mode 100644
index 0000000000..a8a2f07396
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Google/Tests/Factory/GmailTransportFactoryTest.php
@@ -0,0 +1,50 @@
+getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('smtp', 'gmail'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ yield [
+ new Dsn('smtp', 'gmail', self::USER, self::PASSWORD),
+ new GmailTransport(self::USER, self::PASSWORD, $this->getDispatcher(), $this->getLogger()),
+ ];
+ }
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ yield [new Dsn('http', 'gmail', self::USER, self::PASSWORD)];
+ }
+
+ public function incompleteDsnProvider(): iterable
+ {
+ yield [new Dsn('smtp', 'gmail', self::USER)];
+
+ yield [new Dsn('smtp', 'gmail', null, self::PASSWORD)];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Factory/MandrillTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Factory/MandrillTransportFactory.php
new file mode 100644
index 0000000000..265302fa8b
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Factory/MandrillTransportFactory.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Mailchimp\Factory;
+
+use Symfony\Component\Mailer\Bridge\Mailchimp;
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class MandrillTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ $scheme = $dsn->getScheme();
+ $user = $this->getUser($dsn);
+
+ if ('api' === $scheme) {
+ return new Mailchimp\Http\Api\MandrillTransport($user, $this->client, $this->dispatcher, $this->logger);
+ }
+
+ if ('http' === $scheme) {
+ return new Mailchimp\Http\MandrillTransport($user, $this->client, $this->dispatcher, $this->logger);
+ }
+
+ if ('smtp' === $scheme) {
+ $password = $this->getPassword($dsn);
+
+ return new Mailchimp\Smtp\MandrillTransport($user, $password, $this->dispatcher, $this->logger);
+ }
+
+ throw new UnsupportedSchemeException($dsn);
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'mandrill' === $dsn->getHost();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Factory/MandrillTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Factory/MandrillTransportFactoryTest.php
new file mode 100644
index 0000000000..07dbdd4937
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Factory/MandrillTransportFactoryTest.php
@@ -0,0 +1,83 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Mailchimp\Tests\Factory;
+
+use Symfony\Component\Mailer\Bridge\Mailchimp;
+use Symfony\Component\Mailer\Bridge\Mailchimp\Factory\MandrillTransportFactory;
+use Symfony\Component\Mailer\Tests\TransportFactoryTestCase;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
+
+class MandrillTransportFactoryTest extends TransportFactoryTestCase
+{
+ public function getFactory(): TransportFactoryInterface
+ {
+ return new MandrillTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('api', 'mandrill'),
+ true,
+ ];
+
+ yield [
+ new Dsn('http', 'mandrill'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'mandrill'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ $client = $this->getClient();
+ $dispatcher = $this->getDispatcher();
+ $logger = $this->getLogger();
+
+ yield [
+ new Dsn('api', 'mandrill', self::USER),
+ new Mailchimp\Http\Api\MandrillTransport(self::USER, $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('http', 'mandrill', self::USER),
+ new Mailchimp\Http\MandrillTransport(self::USER, $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('smtp', 'mandrill', self::USER, self::PASSWORD),
+ new Mailchimp\Smtp\MandrillTransport(self::USER, self::PASSWORD, $dispatcher, $logger),
+ ];
+ }
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ yield [new Dsn('foo', 'mandrill', self::USER)];
+ }
+
+ public function incompleteDsnProvider(): iterable
+ {
+ yield [new Dsn('api', 'mandrill')];
+
+ yield [new Dsn('smtp', 'mandrill', self::USER)];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Factory/MailgunTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Factory/MailgunTransportFactory.php
new file mode 100644
index 0000000000..3cb4369eb3
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Factory/MailgunTransportFactory.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Mailgun\Factory;
+
+use Symfony\Component\Mailer\Bridge\Mailgun;
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class MailgunTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ $scheme = $dsn->getScheme();
+ $user = $this->getUser($dsn);
+ $password = $this->getPassword($dsn);
+ $region = $dsn->getOption('region');
+
+ if ('api' === $scheme) {
+ return new Mailgun\Http\Api\MailgunTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger);
+ }
+
+ if ('http' === $scheme) {
+ return new Mailgun\Http\MailgunTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger);
+ }
+
+ if ('smtp' === $scheme) {
+ return new Mailgun\Smtp\MailgunTransport($user, $password, $region, $this->dispatcher, $this->logger);
+ }
+
+ throw new UnsupportedSchemeException($dsn);
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'mailgun' === $dsn->getHost();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Factory/MailgunTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Factory/MailgunTransportFactoryTest.php
new file mode 100644
index 0000000000..c65b372d4c
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Factory/MailgunTransportFactoryTest.php
@@ -0,0 +1,79 @@
+getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('api', 'mailgun'),
+ true,
+ ];
+
+ yield [
+ new Dsn('http', 'mailgun'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'mailgun'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ $client = $this->getClient();
+ $dispatcher = $this->getDispatcher();
+ $logger = $this->getLogger();
+
+ yield [
+ new Dsn('api', 'mailgun', self::USER, self::PASSWORD),
+ new Mailgun\Http\Api\MailgunTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('api', 'mailgun', self::USER, self::PASSWORD, null, ['region' => 'eu']),
+ new Mailgun\Http\Api\MailgunTransport(self::USER, self::PASSWORD, 'eu', $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('http', 'mailgun', self::USER, self::PASSWORD),
+ new Mailgun\Http\MailgunTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('smtp', 'mailgun', self::USER, self::PASSWORD),
+ new Mailgun\Smtp\MailgunTransport(self::USER, self::PASSWORD, null, $dispatcher, $logger),
+ ];
+ }
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ yield [new Dsn('foo', 'mailgun', self::USER, self::PASSWORD)];
+ }
+
+ public function incompleteDsnProvider(): iterable
+ {
+ yield [new Dsn('api', 'mailgun', self::USER)];
+
+ yield [new Dsn('api', 'mailgun', null, self::PASSWORD)];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Factory/PostmarkTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Factory/PostmarkTransportFactory.php
new file mode 100644
index 0000000000..0a67102120
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Factory/PostmarkTransportFactory.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Postmark\Factory;
+
+use Symfony\Component\Mailer\Bridge\Postmark;
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class PostmarkTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ $scheme = $dsn->getScheme();
+ $user = $this->getUser($dsn);
+
+ if ('api' === $scheme) {
+ return new Postmark\Http\Api\PostmarkTransport($user, $this->client, $this->dispatcher, $this->logger);
+ }
+
+ if ('smtp' === $scheme) {
+ return new Postmark\Smtp\PostmarkTransport($user, $this->dispatcher, $this->logger);
+ }
+
+ throw new UnsupportedSchemeException($dsn);
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'postmark' === $dsn->getHost();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Factory/PostmarkTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Factory/PostmarkTransportFactoryTest.php
new file mode 100644
index 0000000000..0de2e35aea
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Factory/PostmarkTransportFactoryTest.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Postmark\Tests\Factory;
+
+use Symfony\Component\Mailer\Bridge\Postmark;
+use Symfony\Component\Mailer\Bridge\Postmark\Factory\PostmarkTransportFactory;
+use Symfony\Component\Mailer\Tests\TransportFactoryTestCase;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
+
+class PostmarkTransportFactoryTest extends TransportFactoryTestCase
+{
+ public function getFactory(): TransportFactoryInterface
+ {
+ return new PostmarkTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('api', 'postmark'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'postmark'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ $dispatcher = $this->getDispatcher();
+ $logger = $this->getLogger();
+
+ yield [
+ new Dsn('api', 'postmark', self::USER),
+ new Postmark\Http\Api\PostmarkTransport(self::USER, $this->getClient(), $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('smtp', 'postmark', self::USER),
+ new Postmark\Smtp\PostmarkTransport(self::USER, $dispatcher, $logger),
+ ];
+ }
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ yield [new Dsn('foo', 'postmark', self::USER)];
+ }
+
+ public function incompleteDsnProvider(): iterable
+ {
+ yield [new Dsn('api', 'postmark')];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Factory/SendgridTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Factory/SendgridTransportFactory.php
new file mode 100644
index 0000000000..ec7ed3cfdd
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Factory/SendgridTransportFactory.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Sendgrid\Factory;
+
+use Symfony\Component\Mailer\Bridge\Sendgrid;
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class SendgridTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ $key = $this->getUser($dsn);
+
+ if ('api' === $dsn->getScheme()) {
+ return new Sendgrid\Http\Api\SendgridTransport($key, $this->client, $this->dispatcher, $this->logger);
+ }
+
+ if ('smtp' === $dsn->getScheme()) {
+ return new Sendgrid\Smtp\SendgridTransport($key, $this->dispatcher, $this->logger);
+ }
+
+ throw new UnsupportedSchemeException($dsn);
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'sendgrid' === $dsn->getHost();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Factory/SendgridTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Factory/SendgridTransportFactoryTest.php
new file mode 100644
index 0000000000..82ac41e03b
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Factory/SendgridTransportFactoryTest.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Factory;
+
+use Symfony\Component\Mailer\Bridge\Sendgrid;
+use Symfony\Component\Mailer\Bridge\Sendgrid\Factory\SendgridTransportFactory;
+use Symfony\Component\Mailer\Tests\TransportFactoryTestCase;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
+
+class SendgridTransportFactoryTest extends TransportFactoryTestCase
+{
+ public function getFactory(): TransportFactoryInterface
+ {
+ return new SendgridTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('api', 'sendgrid'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'sendgrid'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ $dispatcher = $this->getDispatcher();
+ $logger = $this->getLogger();
+
+ yield [
+ new Dsn('api', 'sendgrid', self::USER),
+ new Sendgrid\Http\Api\SendgridTransport(self::USER, $this->getClient(), $dispatcher, $logger),
+ ];
+
+ yield [
+ new Dsn('smtp', 'sendgrid', self::USER),
+ new Sendgrid\Smtp\SendgridTransport(self::USER, $dispatcher, $logger),
+ ];
+ }
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ yield [new Dsn('foo', 'sendgrid', self::USER)];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md
index 5b2c5c528f..7e2c53504b 100644
--- a/src/Symfony/Component/Mailer/CHANGELOG.md
+++ b/src/Symfony/Component/Mailer/CHANGELOG.md
@@ -6,6 +6,8 @@ CHANGELOG
* [BC BREAK] Transports depend on `Symfony\Contracts\EventDispatcher\EventDispatcherInterface`
instead of `Symfony\Component\EventDispatcher\EventDispatcherInterface`.
+ * Added possibility to register custom transport for dsn by implementing
+ `Symfony\Component\Mailer\Transport\TransportFactoryInterface` and tagging with `mailer.transport_factory` tag in DI.
4.3.0
-----
diff --git a/src/Symfony/Component/Mailer/Exception/IncompleteDsnException.php b/src/Symfony/Component/Mailer/Exception/IncompleteDsnException.php
new file mode 100644
index 0000000000..f2618b65d9
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Exception/IncompleteDsnException.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Exception;
+
+/**
+ * @author Konstantin Myakshin
+ */
+class IncompleteDsnException extends InvalidArgumentException
+{
+}
diff --git a/src/Symfony/Component/Mailer/Exception/UnsupportedHostException.php b/src/Symfony/Component/Mailer/Exception/UnsupportedHostException.php
new file mode 100644
index 0000000000..92af7b2567
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Exception/UnsupportedHostException.php
@@ -0,0 +1,61 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Exception;
+
+use Symfony\Component\Mailer\Bridge;
+use Symfony\Component\Mailer\Transport\Dsn;
+
+/**
+ * @author Konstantin Myakshin
+ */
+class UnsupportedHostException extends LogicException
+{
+ private const HOST_TO_PACKAGE_MAP = [
+ 'gmail' => [
+ 'class' => Bridge\Google\Factory\GmailTransportFactory::class,
+ 'package' => 'symfony/google-mailer',
+ ],
+ 'mailgun' => [
+ 'class' => Bridge\Mailgun\Factory\MailgunTransportFactory::class,
+ 'package' => 'symfony/mailgun-mailer',
+ ],
+ 'postmark' => [
+ 'class' => Bridge\Postmark\Factory\PostmarkTransportFactory::class,
+ 'package' => 'symfony/postmark-mailer',
+ ],
+ 'sendgrid' => [
+ 'class' => Bridge\Sendgrid\Factory\SendgridTransportFactory::class,
+ 'package' => 'symfony/sendgrid-mailer',
+ ],
+ 'ses' => [
+ 'class' => Bridge\Amazon\Factory\SesTransportFactory::class,
+ 'package' => 'symfony/amazon-mailer',
+ ],
+ 'mandrill' => [
+ 'class' => Bridge\Mailchimp\Factory\MandrillTransportFactory::class,
+ 'package' => 'symfony/mailchimp-mailer',
+ ],
+ ];
+
+ public function __construct(Dsn $dsn)
+ {
+ $host = $dsn->getHost();
+ $package = self::HOST_TO_PACKAGE_MAP[$host] ?? null;
+ if ($package && !class_exists($package['class'])) {
+ parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed. Try running "composer require %s".', $host, $package['package']));
+
+ return;
+ }
+
+ parent::__construct(sprintf('The "%s" mailer is not supported.', $host));
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php
new file mode 100644
index 0000000000..8457378c46
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Exception;
+
+use Symfony\Component\Mailer\Transport\Dsn;
+
+/**
+ * @author Konstantin Myakshin
+ */
+class UnsupportedSchemeException extends LogicException
+{
+ public function __construct(Dsn $dsn)
+ {
+ parent::__construct(sprintf('The "%s" scheme is not supported for mailer "%s".', $dsn->getScheme(), $dsn->getHost()));
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php b/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php
new file mode 100644
index 0000000000..04f12030da
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Tests/Transport/DsnTest.php
@@ -0,0 +1,88 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Tests\Transport;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Mailer\Exception\InvalidArgumentException;
+use Symfony\Component\Mailer\Transport\Dsn;
+
+class DsnTest extends TestCase
+{
+ /**
+ * @dataProvider fromStringProvider
+ */
+ public function testFromString(string $string, Dsn $dsn): void
+ {
+ $this->assertEquals($dsn, Dsn::fromString($string));
+ }
+
+ public function testGetOption(): void
+ {
+ $options = ['with_value' => 'some value', 'nullable' => null];
+ $dsn = new Dsn('smtp', 'example.com', null, null, null, $options);
+
+ $this->assertSame('some value', $dsn->getOption('with_value'));
+ $this->assertSame('default', $dsn->getOption('nullable', 'default'));
+ $this->assertSame('default', $dsn->getOption('not_existent_property', 'default'));
+ }
+
+ /**
+ * @dataProvider invalidDsnProvider
+ */
+ public function testInvalidDsn(string $dsn, string $exceptionMessage): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage($exceptionMessage);
+ Dsn::fromString($dsn);
+ }
+
+ public function fromStringProvider(): iterable
+ {
+ yield 'simple smtp without user and pass' => [
+ 'smtp://example.com',
+ new Dsn('smtp', 'example.com'),
+ ];
+
+ yield 'simple smtp with custom port' => [
+ 'smtp://user1:pass2@example.com:99',
+ new Dsn('smtp', 'example.com', 'user1', 'pass2', 99),
+ ];
+
+ yield 'gmail smtp with urlencoded user and pass' => [
+ 'smtp://u%24er:pa%24s@gmail',
+ new Dsn('smtp', 'gmail', 'u$er', 'pa$s'),
+ ];
+
+ yield 'mailgun api with custom options' => [
+ 'api://u%24er:pa%24s@mailgun?region=eu',
+ new Dsn('api', 'mailgun', 'u$er', 'pa$s', null, ['region' => 'eu']),
+ ];
+ }
+
+ public function invalidDsnProvider(): iterable
+ {
+ yield [
+ 'some://',
+ 'The "some://" mailer DSN is invalid.',
+ ];
+
+ yield [
+ '//sendmail',
+ 'The "//sendmail" mailer DSN must contain a transport scheme.',
+ ];
+
+ yield [
+ 'file:///some/path',
+ 'The "file:///some/path" mailer DSN must contain a mailer name.',
+ ];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Tests/Transport/NullTransportFactoryTest.php b/src/Symfony/Component/Mailer/Tests/Transport/NullTransportFactoryTest.php
new file mode 100644
index 0000000000..8b8ab4a8cd
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Tests/Transport/NullTransportFactoryTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Tests\Transport;
+
+use Symfony\Component\Mailer\Tests\TransportFactoryTestCase;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\NullTransport;
+use Symfony\Component\Mailer\Transport\NullTransportFactory;
+use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
+
+class NullTransportFactoryTest extends TransportFactoryTestCase
+{
+ public function getFactory(): TransportFactoryInterface
+ {
+ return new NullTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('smtp', 'null'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ yield [
+ new Dsn('smtp', 'null'),
+ new NullTransport($this->getDispatcher(), $this->getLogger()),
+ ];
+ }
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ yield [new Dsn('foo', 'null')];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Tests/Transport/SendmailTransportFactoryTest.php b/src/Symfony/Component/Mailer/Tests/Transport/SendmailTransportFactoryTest.php
new file mode 100644
index 0000000000..d5ee4bec52
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Tests/Transport/SendmailTransportFactoryTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Tests\Transport;
+
+use Symfony\Component\Mailer\Tests\TransportFactoryTestCase;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\SendmailTransport;
+use Symfony\Component\Mailer\Transport\SendmailTransportFactory;
+use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
+
+class SendmailTransportFactoryTest extends TransportFactoryTestCase
+{
+ public function getFactory(): TransportFactoryInterface
+ {
+ return new SendmailTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('smtp', 'sendmail'),
+ true,
+ ];
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ yield [
+ new Dsn('smtp', 'sendmail'),
+ new SendmailTransport(null, $this->getDispatcher(), $this->getLogger()),
+ ];
+ }
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ yield [new Dsn('http', 'sendmail')];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php
new file mode 100644
index 0000000000..3a76d46fe1
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php
@@ -0,0 +1,52 @@
+getDispatcher(), $this->getClient(), $this->getLogger());
+ }
+
+ public function supportsProvider(): iterable
+ {
+ yield [
+ new Dsn('smtp', 'example.com'),
+ true,
+ ];
+
+ yield [
+ new Dsn('api', 'example.com'),
+ false,
+ ];
+ }
+
+ public function createProvider(): iterable
+ {
+ $eventDispatcher = $this->getDispatcher();
+ $logger = $this->getLogger();
+
+ $transport = new EsmtpTransport('example.com', 25, null, null, $eventDispatcher, $logger);
+
+ yield [
+ new Dsn('smtp', 'example.com'),
+ $transport,
+ ];
+
+ $transport = new EsmtpTransport('example.com', 99, 'ssl', 'login', $eventDispatcher, $logger);
+ $transport->setUsername(self::USER);
+ $transport->setPassword(self::PASSWORD);
+
+ yield [
+ new Dsn('smtp', 'example.com', self::USER, self::PASSWORD, 99, ['encryption' => 'ssl', 'auth_mode' => 'login']),
+ $transport,
+ ];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Tests/TransportFactoryTestCase.php b/src/Symfony/Component/Mailer/Tests/TransportFactoryTestCase.php
new file mode 100644
index 0000000000..b17f81c1e6
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Tests/TransportFactoryTestCase.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Mailer\Exception\IncompleteDsnException;
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+
+abstract class TransportFactoryTestCase extends TestCase
+{
+ protected const USER = 'u$er';
+ protected const PASSWORD = 'pa$s';
+
+ protected $dispatcher;
+ protected $client;
+ protected $logger;
+
+ abstract public function getFactory(): TransportFactoryInterface;
+
+ abstract public function supportsProvider(): iterable;
+
+ abstract public function createProvider(): iterable;
+
+ public function unsupportedSchemeProvider(): iterable
+ {
+ return [];
+ }
+
+ public function incompleteDsnProvider(): iterable
+ {
+ return [];
+ }
+
+ /**
+ * @dataProvider supportsProvider
+ */
+ public function testSupports(Dsn $dsn, bool $supports): void
+ {
+ $factory = $this->getFactory();
+
+ $this->assertSame($supports, $factory->supports($dsn));
+ }
+
+ /**
+ * @dataProvider createProvider
+ */
+ public function testCreate(Dsn $dsn, TransportInterface $transport): void
+ {
+ $factory = $this->getFactory();
+
+ $this->assertEquals($transport, $factory->create($dsn));
+ }
+
+ /**
+ * @dataProvider unsupportedSchemeProvider
+ */
+ public function testUnsupportedSchemeException(Dsn $dsn): void
+ {
+ $factory = $this->getFactory();
+
+ $this->expectException(UnsupportedSchemeException::class);
+ $factory->create($dsn);
+ }
+
+ /**
+ * @dataProvider incompleteDsnProvider
+ */
+ public function testIncompleteDsnException(Dsn $dsn): void
+ {
+ $factory = $this->getFactory();
+
+ $this->expectException(IncompleteDsnException::class);
+ $factory->create($dsn);
+ }
+
+ protected function getDispatcher(): EventDispatcherInterface
+ {
+ return $this->dispatcher ?? $this->dispatcher = $this->createMock(EventDispatcherInterface::class);
+ }
+
+ protected function getClient(): HttpClientInterface
+ {
+ return $this->client ?? $this->client = $this->createMock(HttpClientInterface::class);
+ }
+
+ protected function getLogger(): LoggerInterface
+ {
+ return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class);
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Tests/TransportTest.php b/src/Symfony/Component/Mailer/Tests/TransportTest.php
index 8c7cd99d98..6fb3a1a08d 100644
--- a/src/Symfony/Component/Mailer/Tests/TransportTest.php
+++ b/src/Symfony/Component/Mailer/Tests/TransportTest.php
@@ -12,345 +12,71 @@
namespace Symfony\Component\Mailer\Tests;
use PHPUnit\Framework\TestCase;
-use Psr\Log\LoggerInterface;
-use Symfony\Component\Mailer\Bridge\Amazon;
-use Symfony\Component\Mailer\Bridge\Google;
-use Symfony\Component\Mailer\Bridge\Mailchimp;
-use Symfony\Component\Mailer\Bridge\Mailgun;
-use Symfony\Component\Mailer\Bridge\Postmark;
-use Symfony\Component\Mailer\Bridge\Sendgrid;
-use Symfony\Component\Mailer\Exception\InvalidArgumentException;
-use Symfony\Component\Mailer\Exception\LogicException;
+use Symfony\Component\Mailer\SentMessage;
+use Symfony\Component\Mailer\SmtpEnvelope;
use Symfony\Component\Mailer\Transport;
-use Symfony\Component\Mime\Email;
-use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
-use Symfony\Contracts\HttpClient\HttpClientInterface;
-use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+use Symfony\Component\Mime\RawMessage;
class TransportTest extends TestCase
{
- public function testFromDsnNull()
+ /**
+ * @dataProvider fromStringProvider
+ */
+ public function testFromString(string $dsn, TransportInterface $transport): void
{
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://null', $dispatcher, null, $logger);
- $this->assertInstanceOf(Transport\NullTransport::class, $transport);
- $p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher');
- $p->setAccessible(true);
- $this->assertSame($dispatcher, $p->getValue($transport));
+ $transportFactory = new Transport([new DummyTransportFactory()]);
+
+ $this->assertEquals($transport, $transportFactory->fromString($dsn));
}
- public function testFromDsnSendmail()
+ public function fromStringProvider(): iterable
{
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://sendmail', $dispatcher, null, $logger);
- $this->assertInstanceOf(Transport\SendmailTransport::class, $transport);
- $p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher');
- $p->setAccessible(true);
- $this->assertSame($dispatcher, $p->getValue($transport));
- }
+ $transportA = new DummyTransport('a');
+ $transportB = new DummyTransport('b');
- public function testFromDsnSmtp()
- {
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://localhost:44?auth_mode=plain&encryption=tls', $dispatcher, null, $logger);
- $this->assertInstanceOf(Transport\Smtp\SmtpTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger);
- $this->assertEquals('localhost', $transport->getStream()->getHost());
- $this->assertEquals('plain', $transport->getAuthMode());
- $this->assertTrue($transport->getStream()->isTLS());
- $this->assertEquals(44, $transport->getStream()->getPort());
- }
+ yield 'simple transport' => [
+ 'dummy://a',
+ $transportA,
+ ];
- public function testFromInvalidDsn()
- {
- $this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('The "some://" mailer DSN is invalid.');
- Transport::fromDsn('some://');
- }
+ yield 'failover transport' => [
+ 'dummy://a || dummy://b',
+ new Transport\FailoverTransport([$transportA, $transportB]),
+ ];
- public function testNoScheme()
- {
- $this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('The "//sendmail" mailer DSN must contain a transport scheme.');
- Transport::fromDsn('//sendmail');
- }
-
- public function testFromInvalidDsnNoHost()
- {
- $this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('The "file:///some/path" mailer DSN must contain a mailer name.');
- Transport::fromDsn('file:///some/path');
- }
-
- public function testFromInvalidTransportName()
- {
- $this->expectException(LogicException::class);
- Transport::fromDsn('api://foobar');
- }
-
- public function testFromDsnGmail()
- {
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@gmail', $dispatcher, null, $logger);
- $this->assertInstanceOf(Google\Smtp\GmailTransport::class, $transport);
- $this->assertEquals('u$er', $transport->getUsername());
- $this->assertEquals('pa$s', $transport->getPassword());
- $this->assertProperties($transport, $dispatcher, $logger);
-
- $this->expectException(LogicException::class);
- Transport::fromDsn('http://gmail');
- }
-
- public function testFromDsnMailgun()
- {
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, null, $logger);
- $this->assertInstanceOf(Mailgun\Smtp\MailgunTransport::class, $transport);
- $this->assertEquals('u$er', $transport->getUsername());
- $this->assertEquals('pa$s', $transport->getPassword());
- $this->assertProperties($transport, $dispatcher, $logger);
-
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, null, $logger);
- $this->assertEquals('smtp.mailgun.org', $transport->getStream()->getHost());
-
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, null, $logger);
- $this->assertEquals('smtp.eu.mailgun.org', $transport->getStream()->getHost());
-
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, null, $logger);
- $this->assertEquals('smtp.mailgun.org', $transport->getStream()->getHost());
-
- $client = $this->createMock(HttpClientInterface::class);
- $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
- $this->assertInstanceOf(Mailgun\Http\MailgunTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger, [
- 'key' => 'u$er',
- 'domain' => 'pa$s',
- 'client' => $client,
- ]);
-
- $response = $this->createMock(ResponseInterface::class);
- $response->expects($this->any())->method('getStatusCode')->willReturn(200);
- $message = (new Email())->from('me@me.com')->to('you@you.com')->subject('hello')->text('Hello you');
-
- $client = $this->createMock(HttpClientInterface::class);
- $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response);
- $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
- $transport->send($message);
-
- $client = $this->createMock(HttpClientInterface::class);
- $client->expects($this->once())->method('request')->with('POST', 'https://api.eu.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response);
- $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, $client, $logger);
- $transport->send($message);
-
- $client = $this->createMock(HttpClientInterface::class);
- $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages.mime')->willReturn($response);
- $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger);
- $transport->send($message);
-
- $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
- $this->assertInstanceOf(Mailgun\Http\Api\MailgunTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger, [
- 'key' => 'u$er',
- 'domain' => 'pa$s',
- 'client' => $client,
- ]);
-
- $client = $this->createMock(HttpClientInterface::class);
- $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response);
- $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
- $transport->send($message);
-
- $client = $this->createMock(HttpClientInterface::class);
- $client->expects($this->once())->method('request')->with('POST', 'https://api.eu.mailgun.net/v3/pa%24s/messages')->willReturn($response);
- $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=eu', $dispatcher, $client, $logger);
- $transport->send($message);
-
- $client = $this->createMock(HttpClientInterface::class);
- $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response);
- $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger);
- $transport->send($message);
-
- $message = (new Email())->from('me@me.com')->to('you@you.com')->subject('hello')->html('test');
- $client = $this->createMock(HttpClientInterface::class);
- $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response);
- $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger);
- $transport->send($message);
-
- $stream = fopen('data://text/plain,'.$message->getTextBody(), 'r');
- $message = (new Email())->from('me@me.com')->to('you@you.com')->subject('hello')->html($stream);
- $client = $this->createMock(HttpClientInterface::class);
- $client->expects($this->once())->method('request')->with('POST', 'https://api.mailgun.net/v3/pa%24s/messages')->willReturn($response);
- $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun?region=us', $dispatcher, $client, $logger);
- $transport->send($message);
-
- $this->expectException(LogicException::class);
- Transport::fromDsn('foo://mailgun');
- }
-
- public function testFromDsnPostmark()
- {
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').'@postmark', $dispatcher, null, $logger);
- $this->assertInstanceOf(Postmark\Smtp\PostmarkTransport::class, $transport);
- $this->assertEquals('u$er', $transport->getUsername());
- $this->assertEquals('u$er', $transport->getPassword());
- $this->assertProperties($transport, $dispatcher, $logger);
-
- $client = $this->createMock(HttpClientInterface::class);
- $transport = Transport::fromDsn('api://'.urlencode('u$er').'@postmark', $dispatcher, $client, $logger);
- $this->assertInstanceOf(Postmark\Http\Api\PostmarkTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger, [
- 'key' => 'u$er',
- 'client' => $client,
- ]);
-
- $this->expectException(LogicException::class);
- Transport::fromDsn('http://postmark');
- }
-
- public function testFromDsnSendgrid()
- {
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').'@sendgrid', $dispatcher, null, $logger);
- $this->assertInstanceOf(Sendgrid\Smtp\SendgridTransport::class, $transport);
- $this->assertEquals('apikey', $transport->getUsername());
- $this->assertEquals('u$er', $transport->getPassword());
- $this->assertProperties($transport, $dispatcher, $logger);
-
- $client = $this->createMock(HttpClientInterface::class);
- $transport = Transport::fromDsn('api://'.urlencode('u$er').'@sendgrid', $dispatcher, $client, $logger);
- $this->assertInstanceOf(Sendgrid\Http\Api\SendgridTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger, [
- 'key' => 'u$er',
- 'client' => $client,
- ]);
-
- $this->expectException(LogicException::class);
- Transport::fromDsn('http://sendgrid');
- }
-
- public function testFromDsnAmazonSes()
- {
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, null, $logger);
- $this->assertInstanceOf(Amazon\Smtp\SesTransport::class, $transport);
- $this->assertEquals('u$er', $transport->getUsername());
- $this->assertEquals('pa$s', $transport->getPassword());
- $this->assertContains('.sun.', $transport->getStream()->getHost());
- $this->assertProperties($transport, $dispatcher, $logger);
-
- $client = $this->createMock(HttpClientInterface::class);
- $transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, $client, $logger);
- $this->assertInstanceOf(Amazon\Http\SesTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger, [
- 'accessKey' => 'u$er',
- 'secretKey' => 'pa$s',
- 'region' => 'sun',
- 'client' => $client,
- ]);
-
- $transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, $client, $logger);
- $this->assertInstanceOf(Amazon\Http\Api\SesTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger, [
- 'accessKey' => 'u$er',
- 'secretKey' => 'pa$s',
- 'region' => 'sun',
- 'client' => $client,
- ]);
-
- $this->expectException(LogicException::class);
- Transport::fromDsn('foo://ses');
- }
-
- public function testFromDsnMailchimp()
- {
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mandrill', $dispatcher, null, $logger);
- $this->assertInstanceOf(Mailchimp\Smtp\MandrillTransport::class, $transport);
- $this->assertEquals('u$er', $transport->getUsername());
- $this->assertEquals('pa$s', $transport->getPassword());
- $this->assertProperties($transport, $dispatcher, $logger);
-
- $client = $this->createMock(HttpClientInterface::class);
- $transport = Transport::fromDsn('http://'.urlencode('u$er').'@mandrill', $dispatcher, $client, $logger);
- $this->assertInstanceOf(Mailchimp\Http\MandrillTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger, [
- 'key' => 'u$er',
- 'client' => $client,
- ]);
-
- $transport = Transport::fromDsn('api://'.urlencode('u$er').'@mandrill', $dispatcher, $client, $logger);
- $this->assertInstanceOf(Mailchimp\Http\Api\MandrillTransport::class, $transport);
- $this->assertProperties($transport, $dispatcher, $logger, [
- 'key' => 'u$er',
- 'client' => $client,
- ]);
-
- $this->expectException(LogicException::class);
- Transport::fromDsn('foo://mandrill');
- }
-
- public function testFromDsnFailover()
- {
- $user = 'user';
- $pass = 'pass';
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://example.com || smtp://'.urlencode($user).'@example.com || smtp://'.urlencode($user).':'.urlencode($pass).'@example.com', $dispatcher, null, $logger);
- $this->assertInstanceOf(Transport\FailoverTransport::class, $transport);
- $p = new \ReflectionProperty(Transport\RoundRobinTransport::class, 'transports');
- $p->setAccessible(true);
- $transports = $p->getValue($transport);
- $this->assertCount(3, $transports);
- foreach ($transports as $transport) {
- $this->assertProperties($transport, $dispatcher, $logger);
- }
- $this->assertSame('', $transports[0]->getUsername());
- $this->assertSame('', $transports[0]->getPassword());
- $this->assertSame($user, $transports[1]->getUsername());
- $this->assertSame('', $transports[1]->getPassword());
- $this->assertSame($user, $transports[2]->getUsername());
- $this->assertSame($pass, $transports[2]->getPassword());
- }
-
- public function testFromDsnRoundRobin()
- {
- $dispatcher = $this->createMock(EventDispatcherInterface::class);
- $logger = $this->createMock(LoggerInterface::class);
- $transport = Transport::fromDsn('smtp://null && smtp://null && smtp://null', $dispatcher, null, $logger);
- $this->assertInstanceOf(Transport\RoundRobinTransport::class, $transport);
- $p = new \ReflectionProperty(Transport\RoundRobinTransport::class, 'transports');
- $p->setAccessible(true);
- $transports = $p->getValue($transport);
- $this->assertCount(3, $transports);
- foreach ($transports as $transport) {
- $this->assertProperties($transport, $dispatcher, $logger);
- }
- }
-
- private function assertProperties(Transport\TransportInterface $transport, EventDispatcherInterface $dispatcher, LoggerInterface $logger, array $props = [])
- {
- $p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher');
- $p->setAccessible(true);
- $this->assertSame($dispatcher, $p->getValue($transport));
-
- $p = new \ReflectionProperty(Transport\AbstractTransport::class, 'logger');
- $p->setAccessible(true);
- $this->assertSame($logger, $p->getValue($transport));
-
- foreach ($props as $prop => $value) {
- $p = new \ReflectionProperty($transport, $prop);
- $p->setAccessible(true);
- $this->assertEquals($value, $p->getValue($transport));
- }
+ yield 'round robin transport' => [
+ 'dummy://a && dummy://b',
+ new Transport\RoundRobinTransport([$transportA, $transportB]),
+ ];
+ }
+}
+
+class DummyTransport implements Transport\TransportInterface
+{
+ private $host;
+
+ public function __construct(string $host)
+ {
+ $this->host = $host;
+ }
+
+ public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage
+ {
+ throw new \BadMethodCallException('This method newer should be called.');
+ }
+}
+
+class DummyTransportFactory implements Transport\TransportFactoryInterface
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ return new DummyTransport($dsn->getHost());
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'dummy' === $dsn->getScheme();
}
}
diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php
index 42f545f9fd..b167b17d8c 100644
--- a/src/Symfony/Component/Mailer/Transport.php
+++ b/src/Symfony/Component/Mailer/Transport.php
@@ -12,181 +12,107 @@
namespace Symfony\Component\Mailer;
use Psr\Log\LoggerInterface;
-use Symfony\Component\Mailer\Bridge\Amazon;
-use Symfony\Component\Mailer\Bridge\Google;
-use Symfony\Component\Mailer\Bridge\Mailchimp;
-use Symfony\Component\Mailer\Bridge\Mailgun;
-use Symfony\Component\Mailer\Bridge\Postmark;
-use Symfony\Component\Mailer\Bridge\Sendgrid;
-use Symfony\Component\Mailer\Exception\InvalidArgumentException;
-use Symfony\Component\Mailer\Exception\LogicException;
+use Symfony\Component\Mailer\Bridge\Amazon\Factory\SesTransportFactory;
+use Symfony\Component\Mailer\Bridge\Google\Factory\GmailTransportFactory;
+use Symfony\Component\Mailer\Bridge\Mailchimp\Factory\MandrillTransportFactory;
+use Symfony\Component\Mailer\Bridge\Mailgun\Factory\MailgunTransportFactory;
+use Symfony\Component\Mailer\Bridge\Postmark\Factory\PostmarkTransportFactory;
+use Symfony\Component\Mailer\Bridge\Sendgrid\Factory\SendgridTransportFactory;
+use Symfony\Component\Mailer\Exception\UnsupportedHostException;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\NullTransportFactory;
+use Symfony\Component\Mailer\Transport\SendmailTransportFactory;
+use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory;
+use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @author Fabien Potencier
+ * @author Konstantin Myakshin
*/
class Transport
{
+ private const FACTORY_CLASSES = [
+ SesTransportFactory::class,
+ GmailTransportFactory::class,
+ MandrillTransportFactory::class,
+ MailgunTransportFactory::class,
+ PostmarkTransportFactory::class,
+ SendgridTransportFactory::class,
+ ];
+
+ private $factories;
+
public static function fromDsn(string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface
{
- // failover?
+ $factory = new self(self::getDefaultFactories($dispatcher, $client, $logger));
+
+ return $factory->fromString($dsn);
+ }
+
+ /**
+ * @param TransportFactoryInterface[] $factories
+ */
+ public function __construct(iterable $factories)
+ {
+ $this->factories = $factories;
+ }
+
+ public function fromString(string $dsn): TransportInterface
+ {
$dsns = preg_split('/\s++\|\|\s++/', $dsn);
if (\count($dsns) > 1) {
- $transports = [];
- foreach ($dsns as $dsn) {
- $transports[] = self::createTransport($dsn, $dispatcher, $client, $logger);
- }
-
- return new Transport\FailoverTransport($transports);
+ return new Transport\FailoverTransport($this->createFromDsns($dsns));
}
- // round robin?
$dsns = preg_split('/\s++&&\s++/', $dsn);
if (\count($dsns) > 1) {
- $transports = [];
- foreach ($dsns as $dsn) {
- $transports[] = self::createTransport($dsn, $dispatcher, $client, $logger);
- }
-
- return new Transport\RoundRobinTransport($transports);
+ return new Transport\RoundRobinTransport($this->createFromDsns($dsns));
}
- return self::createTransport($dsn, $dispatcher, $client, $logger);
+ return $this->fromDsnObject(Dsn::fromString($dsn));
}
- private static function createTransport(string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface
+ public function fromDsnObject(Dsn $dsn): TransportInterface
{
- if (false === $parsedDsn = parse_url($dsn)) {
- throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn));
+ foreach ($this->factories as $factory) {
+ if ($factory->supports($dsn)) {
+ return $factory->create($dsn);
+ }
}
- if (!isset($parsedDsn['scheme'])) {
- throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a transport scheme.', $dsn));
+ throw new UnsupportedHostException($dsn);
+ }
+
+ /**
+ * @param string[] $dsns
+ *
+ * @return TransportInterface[]
+ */
+ private function createFromDsns(array $dsns): array
+ {
+ $transports = [];
+ foreach ($dsns as $dsn) {
+ $transports[] = $this->fromDsnObject(Dsn::fromString($dsn));
}
- if (!isset($parsedDsn['host'])) {
- throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a mailer name.', $dsn));
+ return $transports;
+ }
+
+ private static function getDefaultFactories(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): iterable
+ {
+ foreach (self::FACTORY_CLASSES as $factoryClass) {
+ if (class_exists($factoryClass)) {
+ yield new $factoryClass($dispatcher, $client, $logger);
+ }
}
- $user = urldecode($parsedDsn['user'] ?? '');
- $pass = urldecode($parsedDsn['pass'] ?? '');
- parse_str($parsedDsn['query'] ?? '', $query);
+ yield new NullTransportFactory($dispatcher, $client, $logger);
- switch ($parsedDsn['host']) {
- case 'null':
- if ('smtp' === $parsedDsn['scheme']) {
- return new Transport\NullTransport($dispatcher, $logger);
- }
+ yield new SendmailTransportFactory($dispatcher, $client, $logger);
- throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
- case 'sendmail':
- if ('smtp' === $parsedDsn['scheme']) {
- return new Transport\SendmailTransport(null, $dispatcher, $logger);
- }
-
- throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
- case 'gmail':
- if (!class_exists(Google\Smtp\GmailTransport::class)) {
- throw new \LogicException('Unable to send emails via Gmail as the Google bridge is not installed. Try running "composer require symfony/google-mailer".');
- }
-
- if ('smtp' === $parsedDsn['scheme']) {
- return new Google\Smtp\GmailTransport($user, $pass, $dispatcher, $logger);
- }
-
- throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
- case 'mailgun':
- if (!class_exists(Mailgun\Smtp\MailgunTransport::class)) {
- throw new \LogicException('Unable to send emails via Mailgun as the bridge is not installed. Try running "composer require symfony/mailgun-mailer".');
- }
-
- if ('smtp' === $parsedDsn['scheme']) {
- return new Mailgun\Smtp\MailgunTransport($user, $pass, $query['region'] ?? null, $dispatcher, $logger);
- }
- if ('http' === $parsedDsn['scheme']) {
- return new Mailgun\Http\MailgunTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger);
- }
- if ('api' === $parsedDsn['scheme']) {
- return new Mailgun\Http\Api\MailgunTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger);
- }
-
- throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
- case 'postmark':
- if (!class_exists(Postmark\Smtp\PostmarkTransport::class)) {
- throw new \LogicException('Unable to send emails via Postmark as the bridge is not installed. Try running "composer require symfony/postmark-mailer".');
- }
-
- if ('smtp' === $parsedDsn['scheme']) {
- return new Postmark\Smtp\PostmarkTransport($user, $dispatcher, $logger);
- }
- if ('api' === $parsedDsn['scheme']) {
- return new Postmark\Http\Api\PostmarkTransport($user, $client, $dispatcher, $logger);
- }
-
- throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
- case 'sendgrid':
- if (!class_exists(Sendgrid\Smtp\SendgridTransport::class)) {
- throw new \LogicException('Unable to send emails via Sendgrid as the bridge is not installed. Try running "composer require symfony/sendgrid-mailer".');
- }
-
- if ('smtp' === $parsedDsn['scheme']) {
- return new Sendgrid\Smtp\SendgridTransport($user, $dispatcher, $logger);
- }
- if ('api' === $parsedDsn['scheme']) {
- return new Sendgrid\Http\Api\SendgridTransport($user, $client, $dispatcher, $logger);
- }
-
- throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
- case 'ses':
- if (!class_exists(Amazon\Smtp\SesTransport::class)) {
- throw new \LogicException('Unable to send emails via Amazon SES as the bridge is not installed. Try running "composer require symfony/amazon-mailer".');
- }
-
- if ('smtp' === $parsedDsn['scheme']) {
- return new Amazon\Smtp\SesTransport($user, $pass, $query['region'] ?? null, $dispatcher, $logger);
- }
- if ('api' === $parsedDsn['scheme']) {
- return new Amazon\Http\Api\SesTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger);
- }
- if ('http' === $parsedDsn['scheme']) {
- return new Amazon\Http\SesTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger);
- }
-
- throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
- case 'mandrill':
- if (!class_exists(Mailchimp\Smtp\MandrillTransport::class)) {
- throw new \LogicException('Unable to send emails via Mandrill as the bridge is not installed. Try running "composer require symfony/mailchimp-mailer".');
- }
-
- if ('smtp' === $parsedDsn['scheme']) {
- return new Mailchimp\Smtp\MandrillTransport($user, $pass, $dispatcher, $logger);
- }
- if ('api' === $parsedDsn['scheme']) {
- return new Mailchimp\Http\Api\MandrillTransport($user, $client, $dispatcher, $logger);
- }
- if ('http' === $parsedDsn['scheme']) {
- return new Mailchimp\Http\MandrillTransport($user, $client, $dispatcher, $logger);
- }
-
- throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
- default:
- if ('smtp' === $parsedDsn['scheme']) {
- $transport = new Transport\Smtp\EsmtpTransport($parsedDsn['host'], $parsedDsn['port'] ?? 25, $query['encryption'] ?? null, $query['auth_mode'] ?? null, $dispatcher, $logger);
-
- if ($user) {
- $transport->setUsername($user);
- }
-
- if ($pass) {
- $transport->setPassword($pass);
- }
-
- return $transport;
- }
-
- throw new LogicException(sprintf('The "%s" mailer is not supported.', $parsedDsn['host']));
- }
+ yield new EsmtpTransportFactory($dispatcher, $client, $logger);
}
}
diff --git a/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php b/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php
new file mode 100644
index 0000000000..959fca5746
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/AbstractTransportFactory.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Mailer\Exception\IncompleteDsnException;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+
+/**
+ * @author Konstantin Myakshin
+ */
+abstract class AbstractTransportFactory implements TransportFactoryInterface
+{
+ protected $dispatcher;
+ protected $client;
+ protected $logger;
+
+ public function __construct(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null)
+ {
+ $this->dispatcher = $dispatcher;
+ $this->client = $client;
+ $this->logger = $logger;
+ }
+
+ protected function getUser(Dsn $dsn): string
+ {
+ $user = $dsn->getUser();
+ if (null === $user) {
+ throw new IncompleteDsnException('User is not set.');
+ }
+
+ return $user;
+ }
+
+ protected function getPassword(Dsn $dsn): string
+ {
+ $password = $dsn->getPassword();
+ if (null === $password) {
+ throw new IncompleteDsnException('Password is not set.');
+ }
+
+ return $password;
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Transport/Dsn.php b/src/Symfony/Component/Mailer/Transport/Dsn.php
new file mode 100644
index 0000000000..b5e2843ab4
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/Dsn.php
@@ -0,0 +1,89 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport;
+
+use Symfony\Component\Mailer\Exception\InvalidArgumentException;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class Dsn
+{
+ private $scheme;
+ private $host;
+ private $user;
+ private $password;
+ private $port;
+ private $options;
+
+ public function __construct(string $scheme, string $host, ?string $user = null, ?string $password = null, ?int $port = null, array $options = [])
+ {
+ $this->scheme = $scheme;
+ $this->host = $host;
+ $this->user = $user;
+ $this->password = $password;
+ $this->port = $port;
+ $this->options = $options;
+ }
+
+ public static function fromString(string $dsn): self
+ {
+ if (false === $parsedDsn = parse_url($dsn)) {
+ throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn));
+ }
+
+ if (!isset($parsedDsn['scheme'])) {
+ throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a transport scheme.', $dsn));
+ }
+
+ if (!isset($parsedDsn['host'])) {
+ throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a mailer name.', $dsn));
+ }
+
+ $user = urldecode($parsedDsn['user'] ?? null);
+ $password = urldecode($parsedDsn['pass'] ?? null);
+ $port = $parsedDsn['port'] ?? null;
+ parse_str($parsedDsn['query'] ?? '', $query);
+
+ return new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query);
+ }
+
+ public function getScheme(): string
+ {
+ return $this->scheme;
+ }
+
+ public function getHost(): string
+ {
+ return $this->host;
+ }
+
+ public function getUser(): ?string
+ {
+ return $this->user;
+ }
+
+ public function getPassword(): ?string
+ {
+ return $this->password;
+ }
+
+ public function getPort(int $default = null): ?int
+ {
+ return $this->port ?? $default;
+ }
+
+ public function getOption(string $key, $default = null)
+ {
+ return $this->options[$key] ?? $default;
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Transport/NullTransportFactory.php b/src/Symfony/Component/Mailer/Transport/NullTransportFactory.php
new file mode 100644
index 0000000000..34600f7ec3
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/NullTransportFactory.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport;
+
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class NullTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ if ('smtp' === $dsn->getScheme()) {
+ return new NullTransport($this->dispatcher, $this->logger);
+ }
+
+ throw new UnsupportedSchemeException($dsn);
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'null' === $dsn->getHost();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php b/src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php
new file mode 100644
index 0000000000..99e7bbf097
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/SendmailTransportFactory.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport;
+
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class SendmailTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ if ('smtp' === $dsn->getScheme()) {
+ return new SendmailTransport(null, $this->dispatcher, $this->logger);
+ }
+
+ throw new UnsupportedSchemeException($dsn);
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'sendmail' === $dsn->getHost();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php
new file mode 100644
index 0000000000..d1a5c60c5f
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport\Smtp;
+
+use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
+use Symfony\Component\Mailer\Transport\Dsn;
+use Symfony\Component\Mailer\Transport\TransportInterface;
+
+/**
+ * @author Konstantin Myakshin
+ */
+final class EsmtpTransportFactory extends AbstractTransportFactory
+{
+ public function create(Dsn $dsn): TransportInterface
+ {
+ $encryption = $dsn->getOption('encryption');
+ $authMode = $dsn->getOption('auth_mode');
+ $port = $dsn->getPort(25);
+ $host = $dsn->getHost();
+
+ $transport = new EsmtpTransport($host, $port, $encryption, $authMode, $this->dispatcher, $this->logger);
+
+ if ($user = $dsn->getUser()) {
+ $transport->setUsername($user);
+ }
+
+ if ($password = $dsn->getPassword()) {
+ $transport->setPassword($password);
+ }
+
+ return $transport;
+ }
+
+ public function supports(Dsn $dsn): bool
+ {
+ return 'smtp' === $dsn->getScheme();
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Transport/TransportFactoryInterface.php b/src/Symfony/Component/Mailer/Transport/TransportFactoryInterface.php
new file mode 100644
index 0000000000..9785ae81a9
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Transport/TransportFactoryInterface.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Transport;
+
+use Symfony\Component\Mailer\Exception\IncompleteDsnException;
+use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
+
+/**
+ * @author Konstantin Myakshin
+ */
+interface TransportFactoryInterface
+{
+ /**
+ * @throws UnsupportedSchemeException
+ * @throws IncompleteDsnException
+ */
+ public function create(Dsn $dsn): TransportInterface;
+
+ public function supports(Dsn $dsn): bool;
+}
diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
index 48f7892f52..d4e6f15a80 100644
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
@@ -633,14 +633,24 @@ class PropertyAccessor implements PropertyAccessorInterface
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelized = $this->camelize($property);
$singulars = (array) Inflector::singularize($camelized);
+ $errors = [];
if ($useAdderAndRemover) {
- $methods = $this->findAdderAndRemover($reflClass, $singulars);
+ foreach ($this->findAdderAndRemover($reflClass, $singulars) as $methods) {
+ if (3 === \count($methods)) {
+ $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
+ $access[self::ACCESS_ADDER] = $methods[self::ACCESS_ADDER];
+ $access[self::ACCESS_REMOVER] = $methods[self::ACCESS_REMOVER];
+ break;
+ }
- if (null !== $methods) {
- $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
- $access[self::ACCESS_ADDER] = $methods[0];
- $access[self::ACCESS_REMOVER] = $methods[1];
+ if (isset($methods[self::ACCESS_ADDER])) {
+ $errors[] = sprintf('The add method "%s" in class "%s" was found, but the corresponding remove method "%s" was not found', $methods['methods'][self::ACCESS_ADDER], $reflClass->name, $methods['methods'][self::ACCESS_REMOVER]);
+ }
+
+ if (isset($methods[self::ACCESS_REMOVER])) {
+ $errors[] = sprintf('The remove method "%s" in class "%s" was found, but the corresponding add method "%s" was not found', $methods['methods'][self::ACCESS_REMOVER], $reflClass->name, $methods['methods'][self::ACCESS_ADDER]);
+ }
}
}
@@ -664,30 +674,69 @@ class PropertyAccessor implements PropertyAccessorInterface
// we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
$access[self::ACCESS_NAME] = $setter;
- } elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) {
- $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
- $access[self::ACCESS_NAME] = sprintf(
- 'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
- 'the new value must be an array or an instance of \Traversable, '.
- '"%s" given.',
- $property,
- $reflClass->name,
- implode('()", "', $methods),
- \is_object($value) ? \get_class($value) : \gettype($value)
- );
} else {
- $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
- $access[self::ACCESS_NAME] = sprintf(
- 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
- '"__set()" or "__call()" exist and have public access in class "%s".',
- $property,
- implode('', array_map(function ($singular) {
- return '"add'.$singular.'()"/"remove'.$singular.'()", ';
- }, $singulars)),
- $setter,
- $getsetter,
- $reflClass->name
- );
+ foreach ($this->findAdderAndRemover($reflClass, $singulars) as $methods) {
+ if (3 === \count($methods)) {
+ $errors[] = sprintf(
+ 'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
+ 'the new value must be an array or an instance of \Traversable, '.
+ '"%s" given.',
+ $property,
+ $reflClass->name,
+ implode('()", "', [$methods[self::ACCESS_ADDER], $methods[self::ACCESS_REMOVER]]),
+ \is_object($value) ? \get_class($value) : \gettype($value)
+ );
+ }
+ }
+
+ if (!isset($access[self::ACCESS_NAME])) {
+ $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
+
+ $triedMethods = [
+ $setter => 1,
+ $getsetter => 1,
+ '__set' => 2,
+ '__call' => 2,
+ ];
+
+ foreach ($singulars as $singular) {
+ $triedMethods['add'.$singular] = 1;
+ $triedMethods['remove'.$singular] = 1;
+ }
+
+ foreach ($triedMethods as $methodName => $parameters) {
+ if (!$reflClass->hasMethod($methodName)) {
+ continue;
+ }
+
+ $method = $reflClass->getMethod($methodName);
+
+ if (!$method->isPublic()) {
+ $errors[] = sprintf('The method "%s" in class "%s" was found but does not have public access', $methodName, $reflClass->name);
+ continue;
+ }
+
+ if ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) {
+ $errors[] = sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d', $methodName, $reflClass->name, $method->getNumberOfRequiredParameters(), $parameters);
+ }
+ }
+
+ if (\count($errors)) {
+ $access[self::ACCESS_NAME] = implode('. ', $errors).'.';
+ } else {
+ $access[self::ACCESS_NAME] = sprintf(
+ 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
+ '"__set()" or "__call()" exist and have public access in class "%s".',
+ $property,
+ implode('', array_map(function ($singular) {
+ return '"add'.$singular.'()"/"remove'.$singular.'()", ';
+ }, $singulars)),
+ $setter,
+ $getsetter,
+ $reflClass->name
+ );
+ }
+ }
}
}
@@ -751,13 +800,21 @@ class PropertyAccessor implements PropertyAccessorInterface
foreach ($singulars as $singular) {
$addMethod = 'add'.$singular;
$removeMethod = 'remove'.$singular;
+ $result = ['methods' => [self::ACCESS_ADDER => $addMethod, self::ACCESS_REMOVER => $removeMethod]];
$addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1);
+
+ if ($addMethodFound) {
+ $result[self::ACCESS_ADDER] = $addMethod;
+ }
+
$removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1);
- if ($addMethodFound && $removeMethodFound) {
- return [$addMethod, $removeMethod];
+ if ($removeMethodFound) {
+ $result[self::ACCESS_REMOVER] = $removeMethod;
}
+
+ yield $result;
}
}
diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestAdderRemoverInvalidArgumentLength.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestAdderRemoverInvalidArgumentLength.php
new file mode 100644
index 0000000000..4676bbcafe
--- /dev/null
+++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestAdderRemoverInvalidArgumentLength.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
+
+class TestAdderRemoverInvalidArgumentLength
+{
+ public function addFoo()
+ {
+ }
+
+ public function removeFoo($var1, $var2)
+ {
+ }
+
+ public function setBar($var1, $var2)
+ {
+ }
+}
diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestAdderRemoverInvalidMethods.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestAdderRemoverInvalidMethods.php
new file mode 100644
index 0000000000..5c23f8b188
--- /dev/null
+++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestAdderRemoverInvalidMethods.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
+
+class TestAdderRemoverInvalidMethods
+{
+ public function addFoo($foo)
+ {
+ }
+
+ public function removeBar($foo)
+ {
+ }
+}
diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassSetValue.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassSetValue.php
index f0a7f1f47c..9161f120ff 100644
--- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassSetValue.php
+++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassSetValue.php
@@ -29,4 +29,8 @@ class TestClassSetValue
{
$this->value = $value;
}
+
+ private function setFoo()
+ {
+ }
}
diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php
index b91d1e62eb..2593f61118 100644
--- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php
+++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php
@@ -189,7 +189,7 @@ abstract class PropertyAccessorCollectionTest extends PropertyAccessorArrayAcces
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
- * expectedExceptionMessageRegExp /The property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*" can be defined with the methods "addAxis()", "removeAxis()" but the new value must be an array or an instance of \Traversable, "string" given./
+ * @expectedExceptionMessageRegExp /Could not determine access type for property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*": The property "axes" in class "Mock_PropertyAccessorCollectionTest_Car[^"]*" can be defined with the methods "addAxis\(\)", "removeAxis\(\)" but the new value must be an array or an instance of \\Traversable, "string" given./
*/
public function testSetValueFailsIfAdderAndRemoverExistButValueIsNotTraversable()
{
diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
index d0cbccf1ec..dfd16d9f51 100644
--- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
+++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
@@ -17,6 +17,8 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped;
+use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidArgumentLength;
+use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidMethods;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall;
@@ -762,4 +764,54 @@ class PropertyAccessorTest extends TestCase
$this->assertEquals(['aeroplane'], $object->getAircraft());
}
+
+ /**
+ * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
+ * @expectedExceptionMessageRegExp /.*The add method "addFoo" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidMethods" was found, but the corresponding remove method "removeFoo" was not found\./
+ */
+ public function testAdderWithoutRemover()
+ {
+ $object = new TestAdderRemoverInvalidMethods();
+ $this->propertyAccessor->setValue($object, 'foos', [1, 2]);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
+ * @expectedExceptionMessageRegExp /.*The remove method "removeBar" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidMethods" was found, but the corresponding add method "addBar" was not found\./
+ */
+ public function testRemoverWithoutAdder()
+ {
+ $object = new TestAdderRemoverInvalidMethods();
+ $this->propertyAccessor->setValue($object, 'bars', [1, 2]);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
+ * @expectedExceptionMessageRegExp /.*The method "addFoo" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidArgumentLength" requires 0 arguments, but should accept only 1\. The method "removeFoo" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidArgumentLength" requires 2 arguments, but should accept only 1\./
+ */
+ public function testAdderAndRemoveNeedsTheExactParametersDefined()
+ {
+ $object = new TestAdderRemoverInvalidArgumentLength();
+ $this->propertyAccessor->setValue($object, 'foo', [1, 2]);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
+ * @expectedExceptionMessageRegExp /.*The method "setBar" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestAdderRemoverInvalidArgumentLength" requires 2 arguments, but should accept only 1\./
+ */
+ public function testSetterNeedsTheExactParametersDefined()
+ {
+ $object = new TestAdderRemoverInvalidArgumentLength();
+ $this->propertyAccessor->setValue($object, 'bar', [1, 2]);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException
+ * @expectedExceptionMessageRegExp /.*The method "setFoo" in class "Symfony\\Component\\PropertyAccess\\Tests\\Fixtures\\TestClassSetValue" was found but does not have public access./
+ */
+ public function testSetterNeedsPublicAccess()
+ {
+ $object = new TestClassSetValue(0);
+ $this->propertyAccessor->setValue($object, 'foo', 1);
+ }
}
diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md
index b9994c9175..b74f45b23b 100644
--- a/src/Symfony/Component/Validator/CHANGELOG.md
+++ b/src/Symfony/Component/Validator/CHANGELOG.md
@@ -32,9 +32,9 @@ CHANGELOG
from an array or object
* added the `min_limit_path` and `max_limit_path` parameters in violations when using
`Range` constraint with respectively the `minPropertyPath` and
- `maxPropertyPath` options.
- * added a new `notInRangeMessage` options to the `Range` constraint that will
- be used in the violation builder when both `min` and `max` are not null.
+ `maxPropertyPath` options
+ * added a new `notInRangeMessage` option to the `Range` constraint that will
+ be used in the violation builder when both `min` and `max` are not null
4.3.0
-----
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf
index 478ca19753..3b2eb4131b 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.nl.xlf
@@ -362,6 +362,10 @@
This password has been leaked in a data breach, it must not be used. Please use another password.
Dit wachtwoord is gelekt vanwege een data-inbreuk, het moet niet worden gebruikt. Kies een ander wachtwoord.
+
+ This value should be between {{ min }} and {{ max }}.
+ Deze waarde moet zich tussen {{ min }} en {{ max }} bevinden.
+