diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d865a990b3808..fd91bf6fa7c63 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -108,6 +108,7 @@ use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory as SendinblueNotifierTransportFactory; use Symfony\Component\Notifier\Bridge\Sinch\SinchTransportFactory; use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransportFactory; @@ -2217,6 +2218,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ MobytTransportFactory::class => 'notifier.transport_factory.mobyt', SmsapiTransportFactory::class => 'notifier.transport_factory.smsapi', EsendexTransportFactory::class => 'notifier.transport_factory.esendex', + SendinblueNotifierTransportFactory::class => 'notifier.transport_factory.sendinblue', ]; 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 ee9ed662ff71c..37e339734ea52 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -22,6 +22,7 @@ use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory; use Symfony\Component\Notifier\Bridge\Sinch\SinchTransportFactory; use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransportFactory; @@ -105,6 +106,10 @@ ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + ->set('notifier.transport_factory.sendinblue', SendinblueTransportFactory::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/Sendinblue/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Sendinblue/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Sendinblue/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Sendinblue/CHANGELOG.md new file mode 100644 index 0000000000000..0d994e934e55a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.2.0 +----- + + * Added the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Sendinblue/LICENSE b/src/Symfony/Component/Notifier/Bridge/Sendinblue/LICENSE new file mode 100644 index 0000000000000..4bf0fef4ff3b0 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/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/Sendinblue/README.md b/src/Symfony/Component/Notifier/Bridge/Sendinblue/README.md new file mode 100644 index 0000000000000..24016686f9a41 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/README.md @@ -0,0 +1,26 @@ +Sendinblue Notifier +=================== + +Provides Sendinblue integration for Symfony Notifier. + +DSN example +----------- + +``` +// .env file +SENDINBLUE_DSN=sendinblue://API_KEY@default?sender=PHONE +``` + +where: + - `API_KEY` is your api key from your Sendinblue account + - `PHONE` is your sender's phone number + +See more info at https://developers.sendinblue.com/reference#sendtransacsms + +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/Sendinblue/SendinblueTransport.php b/src/Symfony/Component/Notifier/Bridge/Sendinblue/SendinblueTransport.php new file mode 100644 index 0000000000000..889bd1c454cf1 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/SendinblueTransport.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\Notifier\Bridge\Sendinblue; + +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; + +/** + * @author Pierre Tondereau + * + * @experimental in 5.2 + */ +final class SendinblueTransport extends AbstractTransport +{ + protected const HOST = 'api.sendinblue.com'; + + private $apiKey; + private $sender; + + public function __construct(string $apiKey, string $sender, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->apiKey = $apiKey; + $this->sender = $sender; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('sendinblue://%s?sender=%s', $this->getEndpoint(), $this->sender); + } + + 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))); + } + + $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/v3/transactionalSMS/sms', [ + 'json' => [ + 'sender' => $this->sender, + 'recipient' => $message->getPhone(), + 'content' => $message->getSubject(), + ], + 'headers' => [ + 'api-key' => $this->apiKey, + ], + ]); + + if (201 !== $response->getStatusCode()) { + $error = $response->toArray(false); + + throw new TransportException('Unable to send the SMS: '.$error['message'], $response); + } + + $success = $response->toArray(false); + + $message = new SentMessage($message, (string) $this); + $message->setMessageId($success['messageId']); + + return $message; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sendinblue/SendinblueTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Sendinblue/SendinblueTransportFactory.php new file mode 100644 index 0000000000000..7f9d1f9b4b78c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/SendinblueTransportFactory.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\Notifier\Bridge\Sendinblue; + +use Symfony\Component\Notifier\Exception\IncompleteDsnException; +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; +use Symfony\Component\Notifier\Transport\TransportInterface; + +/** + * @author Pierre Tondereau + * + * @experimental in 5.2 + */ +final class SendinblueTransportFactory extends AbstractTransportFactory +{ + /** + * @return SendinblueTransport + */ + public function create(Dsn $dsn): TransportInterface + { + if (!$sender = $dsn->getOption('sender')) { + throw new IncompleteDsnException('Missing sender.', $dsn->getOriginalDsn()); + } + + $scheme = $dsn->getScheme(); + $apiKey = $this->getUser($dsn); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + if ('sendinblue' === $scheme) { + return (new SendinblueTransport($apiKey, $sender, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + throw new UnsupportedSchemeException($dsn, 'sendinblue', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['sendinblue']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sendinblue/Tests/SendinblueTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Sendinblue/Tests/SendinblueTransportFactoryTest.php new file mode 100644 index 0000000000000..550ba3472b984 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/Tests/SendinblueTransportFactoryTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sendinblue\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory; +use Symfony\Component\Notifier\Exception\IncompleteDsnException; +use Symfony\Component\Notifier\Transport\Dsn; + +final class SendinblueTransportFactoryTest extends TestCase +{ + public function testCreateWithDsn(): void + { + $factory = $this->initFactory(); + + $dsn = 'sendinblue://apiKey@default?sender=0611223344'; + $transport = $factory->create(Dsn::fromString($dsn)); + $transport->setHost('host.test'); + + $this->assertSame('sendinblue://host.test?sender=0611223344', (string) $transport); + } + + public function testCreateWithNoPhoneThrowsMalformed(): void + { + $factory = $this->initFactory(); + + $this->expectException(IncompleteDsnException::class); + + $dsnIncomplete = 'sendinblue://apiKey@default'; + $factory->create(Dsn::fromString($dsnIncomplete)); + } + + public function testSupportsSendinblueScheme(): void + { + $factory = $this->initFactory(); + + $dsn = 'sendinblue://apiKey@default?sender=0611223344'; + $dsnUnsupported = 'foobarmobile://apiKey@default?sender=0611223344'; + + $this->assertTrue($factory->supports(Dsn::fromString($dsn))); + $this->assertFalse($factory->supports(Dsn::fromString($dsnUnsupported))); + } + + private function initFactory(): SendinblueTransportFactory + { + return new SendinblueTransportFactory(); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sendinblue/Tests/SendinblueTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Sendinblue/Tests/SendinblueTransportTest.php new file mode 100644 index 0000000000000..a296697e4ad14 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/Tests/SendinblueTransportTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sendinblue\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransport; +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 SendinblueTransportTest extends TestCase +{ + public function testToStringContainsProperties(): void + { + $transport = $this->initTransport(); + + $this->assertSame('sendinblue://host.test?sender=0611223344', (string) $transport); + } + + public function testSupportsMessageInterface(): void + { + $transport = $this->initTransport(); + + $this->assertTrue($transport->supports(new SmsMessage('0611223344', 'Hello!'))); + $this->assertFalse($transport->supports($this->createMock(MessageInterface::class), 'Hello!')); + } + + public function testSendNonSmsMessageThrowsException(): void + { + $transport = $this->initTransport(); + + $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(400); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['code' => 400, 'message' => 'bad request'])); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = $this->initTransport($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to send the SMS: bad request'); + $transport->send(new SmsMessage('phone', 'testMessage')); + } + + private function initTransport(?HttpClientInterface $client = null): SendinblueTransport + { + return (new SendinblueTransport( + 'api-key', '0611223344', $client ?: $this->createMock(HttpClientInterface::class) + ))->setHost('host.test'); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sendinblue/composer.json b/src/Symfony/Component/Notifier/Bridge/Sendinblue/composer.json new file mode 100644 index 0000000000000..0503619125ce2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/sendinblue-notifier", + "type": "symfony-bridge", + "description": "Symfony Sendinblue Notifier Bridge", + "keywords": ["sms", "sendinblue", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Pierre Tondereau", + "email": "pierre.tondereau@protonmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "ext-json": "*", + "php": ">=7.2.5", + "symfony/http-client": "^4.3|^5.0", + "symfony/notifier": "^5.2" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Sendinblue\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sendinblue/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Sendinblue/phpunit.xml.dist new file mode 100644 index 0000000000000..f767e78b08d97 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sendinblue/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index 69f725ff03556..2cbc5e688592c 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -20,6 +20,7 @@ use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory; use Symfony\Component\Notifier\Bridge\Sinch\SinchTransportFactory; use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransportFactory; @@ -60,6 +61,7 @@ class Transport MobytTransportFactory::class, SmsapiTransportFactory::class, EsendexTransportFactory::class, + SendinblueTransportFactory::class, ]; private $factories;