From 34fc8c3fc28fa7c3877bf051014f213d8456195a Mon Sep 17 00:00:00 2001 From: Olivier Dolbeau Date: Fri, 24 Apr 2020 21:49:28 +0200 Subject: [PATCH] [Notifier] Add Esendex bridge --- .../FrameworkExtension.php | 2 + .../Resources/config/notifier_transports.php | 5 + .../Notifier/Bridge/Esendex/CHANGELOG.md | 7 ++ .../Bridge/Esendex/EsendexTransport.php | 98 +++++++++++++++++++ .../Esendex/EsendexTransportFactory.php | 47 +++++++++ .../Component/Notifier/Bridge/Esendex/LICENSE | 19 ++++ .../Notifier/Bridge/Esendex/README.md | 28 ++++++ .../Esendex/Tests/EsendexTransportTest.php | 88 +++++++++++++++++ .../Notifier/Bridge/Esendex/composer.json | 35 +++++++ .../Notifier/Bridge/Esendex/phpunit.xml.dist | 31 ++++++ .../Exception/UnsupportedSchemeException.php | 4 + src/Symfony/Component/Notifier/Transport.php | 2 + 12 files changed, 366 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Esendex/EsendexTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Esendex/EsendexTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Esendex/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/Esendex/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Esendex/Tests/EsendexTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Esendex/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Esendex/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f922448df709d..bf775761321f4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -95,6 +95,7 @@ use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; @@ -2129,6 +2130,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ ZulipTransportFactory::class => 'notifier.transport_factory.zulip', MobytTransportFactory::class => 'notifier.transport_factory.mobyt', SmsapiTransportFactory::class => 'notifier.transport_factory.smsapi', + EsendexTransportFactory::class => 'notifier.transport_factory.esendex', ]; foreach ($classToServices as $class => $service) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 93d460dbb49a8..ee9ed662ff71c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; @@ -100,6 +101,10 @@ ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + ->set('notifier.transport_factory.esendex', EsendexTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->set('notifier.transport_factory.null', NullTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md new file mode 100644 index 0000000000000..0d994e934e55a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.2.0 +----- + + * Added the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/EsendexTransport.php b/src/Symfony/Component/Notifier/Bridge/Esendex/EsendexTransport.php new file mode 100644 index 0000000000000..a216a16f98594 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/EsendexTransport.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\Notifier\Bridge\Esendex; + +use Symfony\Component\HttpClient\Exception\JsonException; +use Symfony\Component\HttpClient\Exception\TransportException as HttpClientTransportException; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @experimental in 5.2 + */ +final class EsendexTransport extends AbstractTransport +{ + protected const HOST = 'api.esendex.com'; + + private $token; + private $accountReference; + private $from; + + public function __construct(string $token, string $accountReference, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->token = $token; + $this->accountReference = $accountReference; + $this->from = $from; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('esendex://%s', $this->getEndpoint()); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage; + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof SmsMessage) { + throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" (instance of "%s" given).', __CLASS__, SmsMessage::class, get_debug_type($message))); + } + + $messageData = [ + 'to' => $message->getPhone(), + 'body' => $message->getSubject(), + ]; + if (null !== $this->from) { + $messageData['from'] = $this->from; + } + + $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/v1.0/messagedispatcher', [ + 'auth_basic' => $this->token, + 'json' => [ + 'accountreference' => $this->accountReference, + 'messages' => [$messageData], + ], + ]); + + if (200 === $response->getStatusCode()) { + return new SentMessage($message, (string) $this); + } + + $message = sprintf('Unable to send the SMS: error %d.', $response->getStatusCode()); + + try { + $result = $response->toArray(false); + if (!empty($result['errors'])) { + $error = $result['errors'][0]; + + $message .= sprintf(' Details from Esendex: %s: "%s".', $error['code'], $error['description']); + } + } catch (HttpClientTransportException $e) { + // Catching this exception is useful to keep compatibility, with symfony/http-client < 4.4.10 + // See https://github.com/symfony/symfony/pull/37065 + } catch (JsonException $e) { + } + + throw new TransportException($message, $response); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/EsendexTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Esendex/EsendexTransportFactory.php new file mode 100644 index 0000000000000..f526f5a385151 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/EsendexTransportFactory.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\Notifier\Bridge\Esendex; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; +use Symfony\Component\Notifier\Transport\TransportInterface; + +/** + * @experimental in 5.2 + */ +final class EsendexTransportFactory extends AbstractTransportFactory +{ + /** + * @return EsendexTransport + */ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + $token = $this->getUser($dsn).':'.$this->getPassword($dsn); + $accountReference = $dsn->getOption('accountreference'); + $from = $dsn->getOption('from'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + if ('esendex' === $scheme) { + return (new EsendexTransport($token, $accountReference, $from, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + throw new UnsupportedSchemeException($dsn, 'esendex', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['esendex']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/LICENSE b/src/Symfony/Component/Notifier/Bridge/Esendex/LICENSE new file mode 100644 index 0000000000000..4bf0fef4ff3b0 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019-2020 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/README.md b/src/Symfony/Component/Notifier/Bridge/Esendex/README.md new file mode 100644 index 0000000000000..fd1f142ed76f4 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/README.md @@ -0,0 +1,28 @@ +Esendex Notifier +================ + +Provides Esendex integration for Symfony Notifier. + +DSN example +----------- + +``` +// .env file +ESENDEX_DSN='esendex://EMAIL:PASSWORD@default?accountreference=ACCOUNT_REFERENCE&from=FROM' +``` + +where: + - `EMAIL` is your Esendex account email + - `PASSWORD` is the Esendex API password + - `ACCOUNT_REFERENCE` is the Esendex account reference that the messages should be sent from. + - `FROM` is the alphanumeric originator for the message to appear to originate from. + +See Esendex documentation at https://developers.esendex.com/api-reference#smsapis + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/Tests/EsendexTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Esendex/Tests/EsendexTransportTest.php new file mode 100644 index 0000000000000..09bf2844acc7b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/Tests/EsendexTransportTest.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\Notifier\Bridge\Esendex\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransport; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class EsendexTransportTest extends TestCase +{ + public function testToString(): void + { + $transport = new EsendexTransport('testToken', 'accountReference', 'from', $this->createMock(HttpClientInterface::class)); + $transport->setHost('testHost'); + + $this->assertSame(sprintf('esendex://%s', 'testHost'), (string) $transport); + } + + public function testSupportsSmsMessage(): void + { + $transport = new EsendexTransport('testToken', 'accountReference', 'from', $this->createMock(HttpClientInterface::class)); + + $this->assertTrue($transport->supports(new SmsMessage('phone', 'testSmsMessage'))); + $this->assertFalse($transport->supports($this->createMock(MessageInterface::class))); + } + + public function testSendNonSmsMessageThrows(): void + { + $transport = new EsendexTransport('testToken', 'accountReference', 'from', $this->createMock(HttpClientInterface::class)); + + $this->expectException(LogicException::class); + $transport->send($this->createMock(MessageInterface::class)); + } + + public function testSendWithErrorResponseThrows(): void + { + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(500); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = new EsendexTransport('testToken', 'accountReference', 'from', $client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to send the SMS: error 500.'); + $transport->send(new SmsMessage('phone', 'testMessage')); + } + + public function testSendWithErrorResponseContainingDetailsThrows(): void + { + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(500); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['errors' => [['code' => 'accountreference_invalid', 'description' => 'Invalid Account Reference EX0000000']]])); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = new EsendexTransport('testToken', 'accountReference', 'from', $client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to send the SMS: error 500. Details from Esendex: accountreference_invalid: "Invalid Account Reference EX0000000".'); + $transport->send(new SmsMessage('phone', 'testMessage')); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/composer.json b/src/Symfony/Component/Notifier/Bridge/Esendex/composer.json new file mode 100644 index 0000000000000..375e5d82e3dea --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/esendex-notifier", + "type": "symfony-bridge", + "description": "Symfony Esendex Notifier Bridge", + "keywords": ["sms", "esendex", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^7.2.5", + "symfony/http-client": "^4.4|^5.0", + "symfony/notifier": "^5.2" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Esendex\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Esendex/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Esendex/phpunit.xml.dist new file mode 100644 index 0000000000000..97c9dee157bc0 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Esendex/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index 84e0f5cc7d18e..e13f973816c2d 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -78,6 +78,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Smsapi\SmsapiTransportFactory::class, 'package' => 'symfony/smsapi-notifier', ], + 'esendex' => [ + 'class' => Bridge\Esendex\EsendexTransportFactory::class, + 'package' => 'symfony/esendex-notifier', + ], ]; /** diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index 20726a549663a..69f725ff03556 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Notifier; +use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory; @@ -58,6 +59,7 @@ class Transport ZulipTransportFactory::class, MobytTransportFactory::class, SmsapiTransportFactory::class, + EsendexTransportFactory::class, ]; private $factories;