From 8b926fa068c52a77cbcf71efc192d9ed8fdc76ed Mon Sep 17 00:00:00 2001 From: Paul Rijke Date: Sun, 19 May 2024 12:20:58 +0200 Subject: [PATCH] Create transport --- .gitignore | 3 + CHANGELOG.md | 7 +++ SpryngTransport.php | 94 ++++++++++++++++++++++++++++ SpryngTransportFactory.php | 44 +++++++++++++ Tests/SpryngTransportFactoryTest.php | 53 ++++++++++++++++ Tests/SpryngTransportTest.php | 67 ++++++++++++++++++++ composer.json | 36 +++++++++++ 7 files changed, 304 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 SpryngTransport.php create mode 100644 SpryngTransportFactory.php create mode 100644 Tests/SpryngTransportFactoryTest.php create mode 100644 Tests/SpryngTransportTest.php create mode 100644 composer.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7e873f8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +6.4 +--- + +* Add the bridge diff --git a/SpryngTransport.php b/SpryngTransport.php new file mode 100644 index 0000000..9d1cbb4 --- /dev/null +++ b/SpryngTransport.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Spryng; + +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +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\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Paul Rijke + */ +final class SpryngTransport extends AbstractTransport +{ + protected const HOST = 'rest.spryngsms.com'; + + public function __construct( + #[\SensitiveParameter] private readonly string $apiKey, + private readonly string $sender, + private readonly int|string $route, + ?HttpClientInterface $client = null, + ?EventDispatcherInterface $dispatcher = null + ) { + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('spryng://%s?sender=%s&route=%s', $this->getEndpoint(), $this->sender, $this->route); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage; + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof SmsMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message); + } + + $sender = $message->getFrom() ?: $this->sender; + + $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/v1/messages', [ + 'json' => [ + 'sender' => $sender, + 'encoding' => 'auto', + 'originator' => $sender, + 'recipients' => [$message->getPhone()], + 'body' => $message->getSubject(), + 'route' => $this->route ?? 'business', + ], + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => "Bearer {$this->apiKey}", + ], + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote Spryng server.', $response, 0, $e); + } + + if (201 !== $statusCode) { + $error = $response->toArray(false); + + throw new TransportException('Unable to send the SMS: '.($error['message'] ?? $response->getContent(false)), $response); + } + + $success = $response->toArray(false); + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($success['messageId']); + + return $sentMessage; + } +} diff --git a/SpryngTransportFactory.php b/SpryngTransportFactory.php new file mode 100644 index 0000000..3f89e38 --- /dev/null +++ b/SpryngTransportFactory.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\Notifier\Bridge\Spryng; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Paul Rijke + */ +final class SpryngTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): SpryngTransport + { + $scheme = $dsn->getScheme(); + + if ('spryng' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'spryng', $this->getSupportedSchemes()); + } + + $apiKey = $this->getUser($dsn); + $sender = $dsn->getRequiredOption('sender'); + $route = $dsn->getOption('route') ?? 'business'; + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new SpryngTransport($apiKey, $sender, $route, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['spryng']; + } +} diff --git a/Tests/SpryngTransportFactoryTest.php b/Tests/SpryngTransportFactoryTest.php new file mode 100644 index 0000000..5152602 --- /dev/null +++ b/Tests/SpryngTransportFactoryTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Spryng\Tests; + +use Symfony\Component\Notifier\Bridge\Spryng\SpryngTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +final class SpryngTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): SpryngTransportFactory + { + return new SpryngTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'spryng://host.test?sender=0611223344&route=1234', + 'spryng://apiKey@host.test?sender=0611223344&route=1234', + ]; + } + + public static function supportsProvider(): iterable + { + yield [true, 'spryng://apiKey@default?sender=0611223344']; + yield [false, 'somethingElse://apiKey@default?sender=0611223344']; + } + + public static function incompleteDsnProvider(): iterable + { + yield 'missing api_key' => ['spryng://default?sender=0611223344']; + } + + public static function missingRequiredOptionProvider(): iterable + { + yield 'missing option: sender' => ['spryng://apiKey@host.test']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://apiKey@default?sender=0611223344']; + yield ['somethingElse://apiKey@host']; // missing "sender" option + } +} diff --git a/Tests/SpryngTransportTest.php b/Tests/SpryngTransportTest.php new file mode 100644 index 0000000..dc0154d --- /dev/null +++ b/Tests/SpryngTransportTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Spryng\Tests; + +use PHPUnit\Framework\MockObject\Generator\MockClass; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Spryng\SpryngTransport; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class SpryngTransportTest extends TransportTestCase +{ + public static function createTransport(?HttpClientInterface $client = null): SpryngTransport + { + return (new SpryngTransport('api-key', '0611223344', 'business', $client ?? new MockHttpClient()))->setHost('host.test'); + } + + public static function toStringProvider(): iterable + { + yield ['spryng://host.test?sender=0611223344&route=business', self::createTransport()]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; +// yield [new DummyMessage()]; + } + + public function testSendWithErrorResponseThrowsTransportException() + { + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(400); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['code' => 400, 'message' => 'bad request'])); + + $client = new MockHttpClient(static fn (): ResponseInterface => $response); + + $transport = self::createTransport($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to send the SMS: bad request'); + + $transport->send(new SmsMessage('phone', 'testMessage')); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..df41b99 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/spryng-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Spryng Notifier Bridge", + "keywords": ["spryng", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Paul Rijke", + "email": "paul@ibuildings.nl", + "homepage": "https://github.com/parijke" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "7.1.x-dev", + "phpunit/phpunit": "^11.2@dev" + }, + "autoload": { + "psr-4": {"Symfony\\Component\\Notifier\\Bridge\\Spryng\\": ""}, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +}