diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 8ffb10d409..67a2c8ba84 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -71,6 +71,7 @@ Form FrameworkBundle --------------- + * Deprecated booting the kernel before running `WebTestCase::createClient()`. * Deprecated support for `templating` engine in `TemplateController`, use Twig instead * The `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()` has been deprecated. @@ -90,8 +91,17 @@ HttpFoundation HttpKernel ---------- + * 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` +Lock +---- + + * Deprecated `Symfony\Component\Lock\StoreInterface` in favor of `Symfony\Component\Lock\BlockingStoreInterface` and + `Symfony\Component\Lock\PersistStoreInterface`. + * `Factory` is deprecated, use `LockFactory` instead + Messenger --------- @@ -143,3 +153,8 @@ WebProfilerBundle * Deprecated the `ExceptionController::templateExists()` method * Deprecated the `TemplateManager::templateExists()` method + +WebServerBundle +--------------- + + * The bundle is deprecated and will be removed in 5.0. diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 57735fcb17..b697da7dbf 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -208,6 +208,8 @@ Form FrameworkBundle --------------- + * Dropped support for booting the kernel before running `WebTestCase::createClient()`. `createClient()` will throw an + exception if the kernel was already booted before. * Removed the `framework.templating` option, use Twig instead. * The project dir argument of the constructor of `AssetsInstallCommand` is required. * Removed support for `bundle:controller:action` syntax to reference controllers. Use `serviceOrFqcn::method` @@ -275,6 +277,7 @@ HttpFoundation HttpKernel ---------- + * The `getPublicDir()` method has been added to the `BundleInterface`. * Removed `Client`, use `HttpKernelBrowser` instead * The `Kernel::getRootDir()` and the `kernel.root_dir` parameter have been removed * The `KernelInterface::getName()` and the `kernel.name` parameter have been removed @@ -301,6 +304,13 @@ Intl * Removed `Intl::getLocaleBundle()`, use `Locales` instead * Removed `Intl::getRegionBundle()`, use `Countries` instead +Lock +---- + + * Removed `Symfony\Component\Lock\StoreInterface` in favor of `Symfony\Component\Lock\BlockingStoreInterface` and + `Symfony\Component\Lock\PersistStoreInterface`. + * Removed `Factory`, use `LockFactory` instead + Messenger --------- @@ -583,3 +593,8 @@ Yaml * The parser is now stricter and will throw a `ParseException` when a mapping is found inside a multi-line string. + +WebServerBundle +--------------- + + * The bundle has been removed. diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index 796beaee01..18b13a215c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -540,7 +540,7 @@ class TextDescriptor extends Descriptor try { if (\is_array($controller)) { - $r = new \ReflectionMethod($controller); + $r = new \ReflectionMethod($controller[0], $controller[1]); } elseif ($controller instanceof \Closure) { $r = new \ReflectionFunction($controller); } elseif (method_exists($controller, '__invoke')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 9a830ff290..8753deaf1b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -70,11 +70,18 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Lock\Factory; use Symfony\Component\Lock\Lock; +use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; use Symfony\Component\Lock\PersistStoreInterface; use Symfony\Component\Lock\Store\FlockStore; use Symfony\Component\Lock\Store\StoreFactory; use Symfony\Component\Lock\StoreInterface; +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\Mailer; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBus; @@ -1441,7 +1448,7 @@ class FrameworkExtension extends Extension $container->setDefinition($connectionDefinitionId, $connectionDefinition); } - $storeDefinition = new Definition(StoreInterface::class); + $storeDefinition = new Definition(PersistStoreInterface::class); $storeDefinition->setPublic(false); $storeDefinition->setFactory([StoreFactory::class, 'createStore']); $storeDefinition->setArguments([new Reference($connectionDefinitionId)]); @@ -1486,11 +1493,13 @@ class FrameworkExtension extends Extension $container->setAlias(StoreInterface::class, new Alias('lock.store', false)); $container->setAlias(PersistStoreInterface::class, new Alias('lock.store', false)); $container->setAlias(Factory::class, new Alias('lock.factory', false)); + $container->setAlias(LockFactory::class, new Alias('lock.factory', false)); $container->setAlias(LockInterface::class, new Alias('lock', false)); } else { $container->registerAliasForArgument('lock.'.$resourceName.'.store', StoreInterface::class, $resourceName.'.lock.store'); $container->registerAliasForArgument('lock.'.$resourceName.'.store', PersistStoreInterface::class, $resourceName.'.lock.store'); $container->registerAliasForArgument('lock.'.$resourceName.'.factory', Factory::class, $resourceName.'.lock.factory'); + $container->registerAliasForArgument('lock.'.$resourceName.'.factory', LockFactory::class, $resourceName.'.lock.factory'); $container->registerAliasForArgument('lock.'.$resourceName, LockInterface::class, $resourceName.'.lock'); } } @@ -1793,8 +1802,24 @@ class FrameworkExtension extends Extension } $loader->load('mailer.xml'); + $loader->load('mailer_transports.xml'); $container->getDefinition('mailer.default_transport')->setArgument(0, $config['dsn']); + $classToServices = [ + SesTransportFactory::class => 'mailer.transport_factory.amazon', + GmailTransportFactory::class => 'mailer.transport_factory.gmail', + MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp', + MailgunTransportFactory::class => 'mailer.transport_factory.mailgun', + PostmarkTransportFactory::class => 'mailer.transport_factory.postmark', + SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid', + ]; + + foreach ($classToServices as $class => $service) { + if (!class_exists($class)) { + $container->removeDefinition($service); + } + } + $recipients = $config['envelope']['recipients'] ?? null; $sender = $config['envelope']['sender'] ?? null; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml index ce5b9c8d30..cdefecd176 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml @@ -26,7 +26,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml index cfe98f21dc..becf0d1b71 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml @@ -12,12 +12,13 @@ + + + + - + - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml new file mode 100644 index 0000000000..bddcc67f01 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml index ac0fcb47a3..b329aa075c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml @@ -15,6 +15,7 @@ + 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. + diff --git a/src/Symfony/Component/VarDumper/Test/VarDumperTestTrait.php b/src/Symfony/Component/VarDumper/Test/VarDumperTestTrait.php index 11c9b92659..4eb7a43a84 100644 --- a/src/Symfony/Component/VarDumper/Test/VarDumperTestTrait.php +++ b/src/Symfony/Component/VarDumper/Test/VarDumperTestTrait.php @@ -19,6 +19,29 @@ use Symfony\Component\VarDumper\Dumper\CliDumper; */ trait VarDumperTestTrait { + /** + * @internal + */ + private $varDumperConfig = [ + 'casters' => [], + 'flags' => null, + ]; + + protected function setUpVarDumper(array $casters, int $flags = null): void + { + $this->varDumperConfig['casters'] = $casters; + $this->varDumperConfig['flags'] = $flags; + } + + /** + * @after + */ + protected function tearDownVarDumper(): void + { + $this->varDumperConfig['casters'] = []; + $this->varDumperConfig['flags'] = null; + } + public function assertDumpEquals($expected, $data, $filter = 0, $message = '') { $this->assertSame($this->prepareExpectation($expected, $filter), $this->getDump($data, null, $filter), $message); @@ -31,11 +54,14 @@ trait VarDumperTestTrait protected function getDump($data, $key = null, $filter = 0) { - $flags = getenv('DUMP_LIGHT_ARRAY') ? CliDumper::DUMP_LIGHT_ARRAY : 0; - $flags |= getenv('DUMP_STRING_LENGTH') ? CliDumper::DUMP_STRING_LENGTH : 0; - $flags |= getenv('DUMP_COMMA_SEPARATOR') ? CliDumper::DUMP_COMMA_SEPARATOR : 0; + if (null === $flags = $this->varDumperConfig['flags']) { + $flags = getenv('DUMP_LIGHT_ARRAY') ? CliDumper::DUMP_LIGHT_ARRAY : 0; + $flags |= getenv('DUMP_STRING_LENGTH') ? CliDumper::DUMP_STRING_LENGTH : 0; + $flags |= getenv('DUMP_COMMA_SEPARATOR') ? CliDumper::DUMP_COMMA_SEPARATOR : 0; + } $cloner = new VarCloner(); + $cloner->addCasters($this->varDumperConfig['casters']); $cloner->setMaxItems(-1); $dumper = new CliDumper(null, null, $flags); $dumper->setColors(false); diff --git a/src/Symfony/Component/VarDumper/Tests/Test/VarDumperTestTraitTest.php b/src/Symfony/Component/VarDumper/Tests/Test/VarDumperTestTraitTest.php index a4d489cf34..d055c75090 100644 --- a/src/Symfony/Component/VarDumper/Tests/Test/VarDumperTestTraitTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Test/VarDumperTestTraitTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\VarDumper\Tests\Test; use PHPUnit\Framework\TestCase; +use Symfony\Component\VarDumper\Cloner\Stub; +use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Test\VarDumperTestTrait; class VarDumperTestTraitTest extends TestCase @@ -43,4 +45,34 @@ EODUMP; { $this->assertDumpEquals(new \ArrayObject(['bim' => 'bam']), new \ArrayObject(['bim' => 'bam'])); } + + public function testItCanBeConfigured() + { + $this->setUpVarDumper($casters = [ + \DateTimeInterface::class => static function (\DateTimeInterface $date, array $a, Stub $stub): array { + $stub->class = 'DateTime'; + + return ['date' => $date->format('d/m/Y')]; + }, + ], CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR); + + $this->assertSame(CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR, $this->varDumperConfig['flags']); + $this->assertSame($casters, $this->varDumperConfig['casters']); + + $this->assertDumpEquals(<<tearDownVarDumper(); + + $this->assertNull($this->varDumperConfig['flags']); + $this->assertSame([], $this->varDumperConfig['casters']); + } }