diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d1e6aa073ab6d..4750729efb38c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2745,6 +2745,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\RocketChat\RocketChatTransportFactory::class => 'notifier.transport_factory.rocket-chat', NotifierBridge\Sendberry\SendberryTransportFactory::class => 'notifier.transport_factory.sendberry', NotifierBridge\SimpleTextin\SimpleTextinTransportFactory::class => 'notifier.transport_factory.simple-textin', + NotifierBridge\Sevenio\SevenIoTransportFactory::class => 'notifier.transport_factory.sevenio', NotifierBridge\Sinch\SinchTransportFactory::class => 'notifier.transport_factory.sinch', NotifierBridge\Slack\SlackTransportFactory::class => 'notifier.transport_factory.slack', NotifierBridge\Sms77\Sms77TransportFactory::class => 'notifier.transport_factory.sms77', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 39a44e2786d66..c92d9b4b6ca56 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -90,6 +90,7 @@ 'redlink' => Bridge\Redlink\RedlinkTransportFactory::class, 'ring-central' => Bridge\RingCentral\RingCentralTransportFactory::class, 'sendberry' => Bridge\Sendberry\SendberryTransportFactory::class, + 'sevenio' => Bridge\Sevenio\SevenIoTransportFactory::class, 'simple-textin' => Bridge\SimpleTextin\SimpleTextinTransportFactory::class, 'sinch' => Bridge\Sinch\SinchTransportFactory::class, 'sms-biuras' => Bridge\SmsBiuras\SmsBiurasTransportFactory::class, diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Sevenio/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/.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/Sevenio/.gitignore b/src/Symfony/Component/Notifier/Bridge/Sevenio/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Sevenio/CHANGELOG.md new file mode 100644 index 0000000000000..5be39cbeeb951 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.1 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/LICENSE b/src/Symfony/Component/Notifier/Bridge/Sevenio/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-present 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/Sevenio/README.md b/src/Symfony/Component/Notifier/Bridge/Sevenio/README.md new file mode 100644 index 0000000000000..7b0b2ce579a53 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/README.md @@ -0,0 +1,23 @@ +Seven.io Notifier +================= + +Provides [Seven.io](https://www.seven.io/) integration for Symfony Notifier. + +DSN example +----------- + +``` +SEVENIO_DSN=sevenio://API_KEY@default?from=FROM +``` + +where: + - `API_KEY` is your seven.io API key + - `FROM` is your sender (optional, default: SMS) + +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/Sevenio/SevenIoTransport.php b/src/Symfony/Component/Notifier/Bridge/Sevenio/SevenIoTransport.php new file mode 100644 index 0000000000000..529aaad4903b8 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/SevenIoTransport.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\Sevenio; + +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 Frank Nägler + */ +final class SevenIoTransport extends AbstractTransport +{ + protected const HOST = 'gateway.seven.io'; + + public function __construct( + #[\SensitiveParameter] + private string $apiKey, + private ?string $from = null, + HttpClientInterface $client = null, + EventDispatcherInterface $dispatcher = null, + ) { + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('sevenio://%s%s', $this->getEndpoint(), null !== $this->from ? '?from='.$this->from : ''); + } + + 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); + } + + $response = $this->client->request('POST', sprintf('https://%s/api/sms', $this->getEndpoint()), [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'SentWith' => 'symfony/sevenio-notifier', + 'X-Api-Key' => $this->apiKey, + ], + 'json' => [ + 'from' => $message->getFrom() ?: $this->from, + 'json' => 1, + 'text' => $message->getSubject(), + 'to' => $message->getPhone(), + ], + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote seven.io server.', $response, 0, $e); + } + + if (200 !== $statusCode) { + $error = $response->toArray(false); + + throw new TransportException(sprintf('Unable to send the SMS: "%s" (%s).', $error['description'], $error['code']), $response); + } + + $success = $response->toArray(false); + + if (false === \in_array($success['success'], [100, 101])) { + throw new TransportException(sprintf('Unable to send the SMS: "%s".', $success['success']), $response); + } + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId((int) $success['messages'][0]['id']); + + return $sentMessage; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/SevenIoTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Sevenio/SevenIoTransportFactory.php new file mode 100644 index 0000000000000..c91e4c0803779 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/SevenIoTransportFactory.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sevenio; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Frank Nägler + */ +final class SevenIoTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): SevenIoTransport + { + $scheme = $dsn->getScheme(); + + if ('sevenio' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'sevenio', $this->getSupportedSchemes()); + } + + $apiKey = $this->getUser($dsn); + $from = $dsn->getOption('from'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new SevenIoTransport($apiKey, $from, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['sevenio']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/Tests/SevenIoTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Sevenio/Tests/SevenIoTransportFactoryTest.php new file mode 100644 index 0000000000000..e7130d982b9ac --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/Tests/SevenIoTransportFactoryTest.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\Sevenio\Tests; + +use Symfony\Component\Notifier\Bridge\Sevenio\SevenIoTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +final class SevenIoTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): SevenIoTransportFactory + { + return new SevenIoTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'sevenio://host.test', + 'sevenio://apiKey@host.test', + ]; + + yield [ + 'sevenio://host.test?from=TEST', + 'sevenio://apiKey@host.test?from=TEST', + ]; + } + + public static function incompleteDsnProvider(): iterable + { + yield 'missing api key' => ['sevenio://host?from=TEST']; + } + + public static function supportsProvider(): iterable + { + yield [true, 'sevenio://apiKey@default?from=TEST']; + yield [false, 'somethingElse://apiKey@default?from=TEST']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://apiKey@default?from=FROM']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/Tests/SevenIoTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Sevenio/Tests/SevenIoTransportTest.php new file mode 100644 index 0000000000000..444471c09a47a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/Tests/SevenIoTransportTest.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\Notifier\Bridge\Sevenio\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Sevenio\SevenIoTransport; +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; + +final class SevenIoTransportTest extends TransportTestCase +{ + public static function createTransport(HttpClientInterface $client = null, string $from = null): SevenIoTransport + { + return new SevenIoTransport('apiKey', $from, $client ?? new MockHttpClient()); + } + + public static function toStringProvider(): iterable + { + yield ['sevenio://gateway.seven.io', self::createTransport()]; + yield ['sevenio://gateway.seven.io?from=TEST', self::createTransport(null, 'TEST')]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [new DummyMessage()]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/composer.json b/src/Symfony/Component/Notifier/Bridge/Sevenio/composer.json new file mode 100644 index 0000000000000..328b88904806d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/composer.json @@ -0,0 +1,30 @@ +{ + "name": "symfony/sevenio-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Seven.io Notifier Bridge", + "keywords": ["sms", "sevenio", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Frank Nägler", + "email": "frank@naegler.hamburg" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^7.1" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Sevenio\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sevenio/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Sevenio/phpunit.xml.dist new file mode 100644 index 0000000000000..a4df2d258cad2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sevenio/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 7254ecd84e814..dedd12612d2bf 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -216,6 +216,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Sendberry\SendberryTransportFactory::class, 'package' => 'symfony/sendberry-notifier', ], + 'sevenio' => [ + 'class' => Bridge\Sevenio\SevenIoTransportFactory::class, + 'package' => 'symfony/sevenio-notifier', + ], 'simpletextin' => [ 'class' => Bridge\SimpleTextin\SimpleTextinTransportFactory::class, 'package' => 'symfony/simple-textin-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 4c4815756d5b2..7642d2ae17fd6 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -76,6 +76,7 @@ public static function setUpBeforeClass(): void Bridge\RingCentral\RingCentralTransportFactory::class => false, Bridge\RocketChat\RocketChatTransportFactory::class => false, Bridge\Sendberry\SendberryTransportFactory::class => false, + Bridge\Sevenio\SevenIoTransportFactory::class => false, Bridge\SimpleTextin\SimpleTextinTransportFactory::class => false, Bridge\Sinch\SinchTransportFactory::class => false, Bridge\Slack\SlackTransportFactory::class => false, @@ -158,6 +159,7 @@ public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \ yield ['ringcentral', 'symfony/ring-central-notifier']; yield ['rocketchat', 'symfony/rocket-chat-notifier']; yield ['sendberry', 'symfony/sendberry-notifier']; + yield ['sevenio', 'symfony/sevenio-notifier']; yield ['simpletextin', 'symfony/simple-textin-notifier']; yield ['sinch', 'symfony/sinch-notifier']; yield ['slack', 'symfony/slack-notifier']; diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index e7d238de86461..28059f66beca0 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -77,6 +77,7 @@ final class Transport Bridge\RingCentral\RingCentralTransportFactory::class, Bridge\RocketChat\RocketChatTransportFactory::class, Bridge\Sendberry\SendberryTransportFactory::class, + Bridge\Sevenio\SevenIoTransportFactory::class, Bridge\SimpleTextin\SimpleTextinTransportFactory::class, Bridge\Sinch\SinchTransportFactory::class, Bridge\Slack\SlackTransportFactory::class,