diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f44a61693a..e73863c8ac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -126,6 +126,7 @@ use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory as S use Symfony\Component\Notifier\Bridge\Sinch\SinchTransportFactory; use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransportFactory; +use Symfony\Component\Notifier\Bridge\SpotHit\SpotHitTransportFactory; use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; use Symfony\Component\Notifier\Bridge\Twilio\TwilioTransportFactory; use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; @@ -2236,6 +2237,7 @@ class FrameworkExtension extends Extension AllMySmsTransportFactory::class => 'notifier.transport_factory.allmysms', FirebaseTransportFactory::class => 'notifier.transport_factory.firebase', FreeMobileTransportFactory::class => 'notifier.transport_factory.freemobile', + SpotHitTransportFactory::class => 'notifier.transport_factory.spothit', OvhCloudTransportFactory::class => 'notifier.transport_factory.ovhcloud', SinchTransportFactory::class => 'notifier.transport_factory.sinch', ZulipTransportFactory::class => 'notifier.transport_factory.zulip', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index f57869b0f2..e6e0b39a5c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -34,6 +34,7 @@ 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; +use Symfony\Component\Notifier\Bridge\SpotHit\SpotHitTransportFactory; use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; use Symfony\Component\Notifier\Bridge\Twilio\TwilioTransportFactory; use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; @@ -90,6 +91,10 @@ return static function (ContainerConfigurator $container) { ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + ->set('notifier.transport_factory.spothit', SpotHitTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->set('notifier.transport_factory.ovhcloud', OvhCloudTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/SpotHit/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/SpotHit/CHANGELOG.md new file mode 100644 index 0000000000..1f2b652ac2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.3 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/SpotHit/README.md b/src/Symfony/Component/Notifier/Bridge/SpotHit/README.md new file mode 100644 index 0000000000..b3c8fcaa72 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/README.md @@ -0,0 +1,23 @@ +Spot-Hit Notifier +================= + +Provides [Spot-Hit](https://www.spot-hit.fr/) integration for Symfony Notifier. + +#### DSN example + +``` +SPOTHIT_DSN=spothit://TOKEN@default?from=FROM +``` + +where: + - `TOKEN` is your Spot-Hit API key + - `FROM` is the custom sender (3-11 letters, default is a 5 digits phone number) + +Resources +--------- + + * [Spot-Hit API doc](https://www.spot-hit.fr/documentation-api). + * [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/SpotHit/SpotHitTransport.php b/src/Symfony/Component/Notifier/Bridge/SpotHit/SpotHitTransport.php new file mode 100644 index 0000000000..1e3f362562 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/SpotHitTransport.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\SpotHit; + +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\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author James Hemery + */ +final class SpotHitTransport extends AbstractTransport +{ + protected const HOST = 'spot-hit.fr'; + + private $token; + private $from; + + public function __construct(string $token, ?string $from = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->token = $token; + $this->from = $from; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + if (!$this->from) { + return sprintf('spothit://%s', $this->getEndpoint()); + } + + return sprintf('spothit://%s?from=%s', $this->getEndpoint(), $this->from); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage; + } + + /** + * @param MessageInterface|SmsMessage $message + * + * @throws TransportExceptionInterface + * @throws ClientExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ServerExceptionInterface + */ + protected function doSend(MessageInterface $message): SentMessage + { + if (!$this->supports($message)) { + throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message); + } + + $endpoint = sprintf('https://www.%s/api/envoyer/sms', $this->getEndpoint()); + $response = $this->client->request('POST', $endpoint, [ + 'body' => [ + 'key' => $this->token, + 'destinataires' => $message->getPhone(), + 'type' => 'premium', + 'message' => $message->getSubject(), + 'expediteur' => $this->from, + ], + ]); + + $data = json_decode($response->getContent(), true); + + if (!$data['resultat']) { + $errors = \is_array($data['erreurs']) ? implode(',', $data['erreurs']) : $data['erreurs']; + throw new TransportException(sprintf('[HTTP %d] Unable to send the SMS: error(s) "%s".', $response->getStatusCode(), $errors), $response); + } + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($data['id']); + + return $sentMessage; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SpotHit/SpotHitTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/SpotHit/SpotHitTransportFactory.php new file mode 100644 index 0000000000..912b4e2e0f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/SpotHitTransportFactory.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\SpotHit; + +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 James Hemery + */ +final class SpotHitTransportFactory extends AbstractTransportFactory +{ + /** + * @return SpotHitTransport + */ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + + if ('spothit' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'spothit', $this->getSupportedSchemes()); + } + + $token = $this->getUser($dsn); + $from = $dsn->getOption('from'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new SpotHitTransport($token, $from, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['spothit']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SpotHit/Tests/SpotHitTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/SpotHit/Tests/SpotHitTransportFactoryTest.php new file mode 100644 index 0000000000..1463c845bf --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/Tests/SpotHitTransportFactoryTest.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\Notifier\Bridge\SpotHit\Tests; + +use Symfony\Component\Notifier\Bridge\SpotHit\SpotHitTransportFactory; +use Symfony\Component\Notifier\Tests\TransportFactoryTestCase; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface; + +final class SpotHitTransportFactoryTest extends TransportFactoryTestCase +{ + /** + * @return SpotHitTransportFactory + */ + public function createFactory(): TransportFactoryInterface + { + return new SpotHitTransportFactory(); + } + + public function createProvider(): iterable + { + yield [ + 'spothit://spot-hit.fr', + 'spothit://api_token@default', + ]; + yield [ + 'spothit://spot-hit.fr?from=MyCompany', + 'spothit://api_token@default?from=MyCompany', + ]; + } + + public function supportsProvider(): iterable + { + yield [true, 'spothit://api_token@default?from=MyCompany']; + yield [false, 'somethingElse://api_token@default?from=MyCompany']; + } + + public function unsupportedSchemeProvider(): iterable + { + yield ['foobar://api_token@default?from=MyCompany']; + yield ['foobar://api_token@default']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SpotHit/Tests/SpotHitTransportTest.php b/src/Symfony/Component/Notifier/Bridge/SpotHit/Tests/SpotHitTransportTest.php new file mode 100644 index 0000000000..755e9ec2f3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/Tests/SpotHitTransportTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\SpotHit\Tests; + +use Symfony\Component\Notifier\Bridge\SpotHit\SpotHitTransport; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Tests\TransportTestCase; +use Symfony\Component\Notifier\Transport\TransportInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +final class SpotHitTransportTest extends TransportTestCase +{ + /** + * @return SpotHitTransport + */ + public function createTransport(?HttpClientInterface $client = null): TransportInterface + { + return (new SpotHitTransport('api_token', 'MyCompany', $client ?: $this->createMock(HttpClientInterface::class)))->setHost('host.test'); + } + + public function toStringProvider(): iterable + { + yield ['spothit://host.test?from=MyCompany', $this->createTransport()]; + } + + public function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + yield [new SmsMessage('+33611223344', 'Hello!')]; + } + + public function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [$this->createMock(MessageInterface::class)]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/SpotHit/composer.json b/src/Symfony/Component/Notifier/Bridge/SpotHit/composer.json new file mode 100644 index 0000000000..a24735398d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/composer.json @@ -0,0 +1,34 @@ +{ + "name": "symfony/spothit-notifier", + "type": "symfony-bridge", + "description": "Symfony Spot-Hit Notifier Bridge", + "keywords": ["sms", "spot-hit", "notifier", "symfony"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "James Hemery", + "homepage": "https://github.com/JamesHemery" + }, + { + "name": "Yield Studio", + "homepage": "https://github.com/YieldStudio" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/http-client": "^4.3|^5.1", + "symfony/notifier": "^5.3" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\SpotHit\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/SpotHit/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/SpotHit/phpunit.xml.dist new file mode 100644 index 0000000000..d51b53d424 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/SpotHit/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 1200ccb31f..f147e5485c 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -68,6 +68,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\FreeMobile\FreeMobileTransportFactory::class, 'package' => 'symfony/free-mobile-notifier', ], + 'spothit' => [ + 'class' => Bridge\SpotHit\SpotHitTransportFactory::class, + 'package' => 'symfony/spothit-notifier', + ], 'ovhcloud' => [ 'class' => Bridge\OvhCloud\OvhCloudTransportFactory::class, 'package' => 'symfony/ovh-cloud-notifier',