8000 feature #41375 [Notifier] Add MessageMedia Bridge (vuphuong87) · symfony/symfony@d3757a1 · GitHub
[go: up one dir, main page]

Skip to content

Commit d3757a1

Browse files
committed
feature #41375 [Notifier] Add MessageMedia Bridge (vuphuong87)
This PR was squashed before being merged into the 5.4 branch. Discussion ---------- [Notifier] Add MessageMedia Bridge | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | License | MIT | Doc PR | symfony/symfony-docs#15366 | Recipe PR | symfony/recipes#950 Add MessageMedia bridge to Symfony Notifier - [x] submit changes to the documentation - [x] submit changes to the recipe - [x] finish the code https://messagemedia.github.io/documentation/#operation/SendMessages Commits ------- dd3da8f [Notifier] Add MessageMedia Bridge
2 parents d33ec7b + dd3da8f commit d3757a1

16 files changed

+418
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+3
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
128128
use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory;
129129
use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransport;
130+
use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory;
130131
use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory;
131132
use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory;
132133
use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory;
@@ -2431,6 +2432,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
24312432
MattermostTransportFactory::class => 'notifier.transport_factory.mattermost',
24322433
MercureTransportFactory::class => 'notifier.transport_factory.mercure',
24332434
MessageBirdTransport::class => 'notifier.transport_factory.messagebird',
2435+
MessageMediaTransportFactory::class => 'notifier.transport_factory.messagemedia',
24342436
MicrosoftTeamsTransportFactory::class => 'notifier.transport_factory.microsoftteams',
24352437
MobytTransportFactory::class => 'notifier.transport_factory.mobyt',
24362438
NexmoTransportFactory::class => 'notifier.transport_factory.nexmo',
@@ -2460,6 +2462,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
24602462
case 'lightsms': $package = 'light-sms'; break;
24612463
case 'linkedin': $package = 'linked-in'; break;
24622464
case 'messagebird': $package = 'message-bird'; break;
2465+
case 'messagemedia': $package = 'message-media'; break;
24632466
case 'microsoftteams': $package = 'microsoft-teams'; break;
24642467
case 'ovhcloud': $package = 'ovh-cloud'; break;
24652468
case 'rocketchat': $package = 'rocket-chat'; break;

src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
3030
use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory;
3131
use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransportFactory;
32+
use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory;
3233
use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory;
3334
use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory;
3435
use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory;
@@ -191,6 +192,10 @@
191192
->parent('notifier.transport_factory.abstract')
192193
->tag('texter.transport_factory')
193194

195+
->set('notifier.transport_factory.messagemedia', MessageMediaTransportFactory::class)
196+
->parent('notifier.transport_factory.abstract')
197+
->tag('texter.transport_factory')
198+
194199
->set('notifier.transport_factory.telnyx', TelnyxTransportFactory::class)
195200
->parent('notifier.transport_factory.abstract')
196201
->tag('texter.transport_factory')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
5.4
5+
---
6+
7+
* Add the bridge
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2021 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\MessageMedia;
13+
14+
use Symfony\Component\Notifier\Exception\TransportException;
15+
use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException;
16+
use Symfony\Component\Notifier\Message\MessageInterface;
17+
use Symfony\Component\Notifier\Message\SentMessage;
18+
use Symfony\Component\Notifier\Message\SmsMessage;
19+
use Symfony\Component\Notifier\Transport\AbstractTransport;
20+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
21+
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
22+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
23+
use Symfony\Contracts\HttpClient\HttpClientInterface;
24+
25+
/**
26+
* @author Adrian Nguyen <vuphuong87@gmail.com>
27+
*/
28+
final class MessageMediaTransport extends AbstractTransport
29+
{
30+
protected const HOST = 'api.messagemedia.com';
31+
32+
private $apiKey;
33+
private $apiSecret;
34+
private $from;
35+
36+
public function __construct(string $apiKey, string $apiSecret, string $from = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
37+
{
38+
$this->apiKey = $apiKey;
39+
$this->apiSecret = $apiSecret;
40+
$this->from = $from;
41+
42+
parent::__construct($client, $dispatcher);
43+
}
44+
45+
public function __toString(): string
46+
{
47+
if (null !== $this->from) {
48+
return sprintf('messagemedia://%s?from=%s', $this->getEndpoint(), $this->from);
49+
}
50+
51+
return sprintf('messagemedia://%s', $this->getEndpoint());
52+
}
53+
54+
public function supports(MessageInterface $message): bool
55+
{
56+
return $message instanceof SmsMessage;
57+
}
58+
59+
protected function doSend(MessageInterface $message): SentMessage
60+
{
61+
if (!$message instanceof SmsMessage) {
62+
throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message);
63+
}
64+
65+
$endpoint = sprintf('https://%s/v1/messages', $this->getEndpoint());
66+
$response = $this->client->request(
67+
'POST',
68+
$endpoint,
69+
[
70+
'auth_basic' => $this->apiKey.':'.$this->apiSecret,
71+
'json' => [
72+
'messages' => [
73+
[
74+
'destination_number' => $message->getPhone(),
75+
'source_number' => $this->from,
76+
'content' => $message->getSubject(),
77+
],
78+
],
79+
],
80+
]
81+
);
82+
83+
if (202 === $response->getStatusCode()) {
84+
$result = $response->toArray(false)['messages'][0];
85+
$sentMessage = new SentMessage($message, (string) $this);
86+
$sentMessage->setMessageId($result['message_id']);
87+
88+
return $sentMessage;
89+
}
90+
91+
try {
92+
$error = $response->toArray(false);
93+
94+
$errorMessage = $error['details'][0] ?? ($error['message'] ?? 'Unknown reason');
95+
} catch (DecodingExceptionInterface | TransportExceptionInterface $e) {
96+
$errorMessage = 'Unknown reason';
97+
}
98+
99+
throw new TransportException(sprintf('Unable to send the SMS: "%s".', $errorMessage), $response);
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\MessageMedia;
13+
14+
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
15+
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
16+
use Symfony\Component\Notifier\Transport\Dsn;
17+
use Symfony\Component\Notifier\Transport\TransportInterface;
18+
19+
/**
20+
* @author Adrian Nguyen <vuphuong87@gmail.com>
21+
*/
22+
final class MessageMediaTransportFactory extends AbstractTransportFactory
23+
{
24+
/**
25+
* @return MessageMediaTransport
26+
*/
27+
public function create(Dsn $dsn): TransportInterface
28+
{
29+
$scheme = $dsn->getScheme();
30+
31+
if ('messagemedia' !== $scheme) {
32+
throw new UnsupportedSchemeException($dsn, 'messagemedia', $this->getSupportedSchemes());
33+
}
34+
35+
$apiKey = $this->getUser($dsn);
36+
$apiSecret = $this->getPassword($dsn);
37+
$from = $dsn->getOption('from');
38+
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
39+
$port = $dsn->getPort();
40+
41+
return (new MessageMediaTransport($apiKey, $apiSecret, $from, $this->client, $this->dispatcher))->setHost($host)->setPort($port);
42+
}
43+
44+
protected function getSupportedSchemes(): array
45+
{
46+
return ['messagemedia'];
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
MessageMedia Notifier
2+
=================
3+
4+
Provides [MessageMedia](https://messagemedia.com/) integration for Symfony Notifier.
5+
6+
DSN example
7+
-----------
8+
9+
```
10+
MESSAGEMEDIA_DSN=messagemedia://API_KEY:API_SECRET@default?from=FROM
11+
```
12+
13+
where:
14+
- `API_KEY` is your API key
15+
- `API_SECRET` is your API secret
16+
- `FROM` is your registered sender ID (optional). Accepted values: 3-15 letters, could be alpha tag, shortcode or international phone number.
17+
When phone number starts with a `+` sign, it needs to be url encoded in the DSN
18+
19+
Resources
20+
---------
21+
22+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
23+
* [Report issues](https://github.com/symfony/symfony/issues) and
24+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
25+
in the [main Symfony repository](https://github.com/symfony/symfony)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\MessageMedia\Tests;
13+
14+
use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory;
15+
use Symfony\Component\Notifier\Test\TransportFactoryTestCase;
16+
use Symfony\Component\Notifier\Transport\TransportFactoryInterface;
17+
18+
final class MessageMediaTransportFactoryTest extends TransportFactoryTestCase
19+
{
20+
/**
21+
* @return MessageMediaTransportFactory
22+
*/
23+
public function createFactory(): TransportFactoryInterface
24+
{
25+
return new MessageMediaTransportFactory();
26+
}
27+
28+
public function createProvider(): iterable
29+
{
30+
yield [
31+
'messagemedia://host.test',
32+
'messagemedia://apiKey:apiSecret@host.test',
33+
];
34+
35+
yield [
36+
'messagemedia://host.test?from=TEST',
37+
'messagemedia://apiKey:apiSecret@host.test?from=TEST',
38+
];
39+
}
40+
41+
public function supportsProvider(): iterable
42+
{
43+
yield [true, 'messagemedia://apiKey:apiSecret@default'];
44+
yield [false, 'somethingElse://apiKey:apiSecret@default'];
45+
}
46+
47+
public function unsupportedSchemeProvider(): iterable
48+
{
49+
yield ['somethingElse://apiKey:apiSecret@default'];
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\MessageMedia\Tests;
13+
14+
use Symfony\Component\HttpClient\MockHttpClient;
15+
use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransport;
16+
use Symfony\Component\Notifier\Exception\TransportException;
17+
use Symfony\Component\Notifier\Exception\TransportExceptionInterface;
18+
use Symfony\Component\Notifier\Message\ChatMessage;
19+
use Symfony\Component\Notifier\Message\MessageInterface;
20+
use Symfony\Component\Notifier\Message\SmsMessage;
21+
use Symfony\Component\Notifier\Test\TransportTestCase;
22+
use Symfony\Component\Notifier\Transport\TransportInterface;
23+
use Symfony\Contracts\HttpClient\HttpClientInterface;
24+
use Symfony\Contracts\HttpClient\ResponseInterface;
25+
26+
final class MessageMediaTransportTest extends TransportTestCase
27+
{
28+
/**
29+
* @return MessageMediaTransport
30+
*/
31+
public function createTransport(HttpClientInterface $client = null, string $from = null): TransportInterface
32+
{
33+
return new MessageMediaTransport('apiKey', 'apiSecret', $from, $client ?? $this->createMock(HttpClientInterface::class));
34+
}
35+
36+
public function toStringProvider(): iterable
37+
{
38+
yield ['messagemedia://api.messagemedia.com', $this->createTransport()];
39+
yield ['messagemedia://api.messagemedia.com?from=TEST', $this->createTransport(null, 'TEST')];
40+
}
41+
42+
public function supportedMessagesProvider(): iterable
43+
{
44+
yield [new SmsMessage('0491570156', 'Hello!')];
45+
}
46+
47+
public function unsupportedMessagesProvider(): iterable
48+
{
49+
yield [new ChatMessage('Hello!')];
50+
yield [$this->createMock(MessageInterface::class)];
51+
}
52+
53+
/**
54+
* @dataProvider exceptionIsThrownWhenHttpSendFailedProvider
55+
*
56+
* @throws TransportExceptionInterface
57+
*/
58+
public function testExceptionIsThrownWhenHttpSendFailed(int $statusCode, string $content, string $expectedExceptionMessage)
59+
{
60+
$response = $this->createMock(ResponseInterface::class);
61+
$response->method('getStatusCode')
62+
->willReturn($statusCode);
63+
$response->method('getContent')
64+
->willReturn($content);
65+
66+
$client = new MockHttpClient($response);
67+
68+
$transport = new MessageMediaTransport('apiKey', 'apiSecret', null, $client);
69+
$this->expectException(TransportException::class);
70+
$this->expectExceptionMessage($expectedExceptionMessage);
71+
72+
$transport->send(new SmsMessage('+61491570156', 'Hello!'));
73+
}
74+
75+
public function exceptionIsThrownWhenHttpSendFailedProvider(): iterable
76+
{
77+
yield [503, '', 'Unable to send the SMS: "Unknown reason".'];
78+
yield [500, '{"details": ["Something went wrong."]}', 'Unable to send the SMS: "Something went wrong.".'];
79+
yield [403, '{"message": "Forbidden."}', 'Unable to send the SMS: "Forbidden.'];
80+
yield [401, '{"Unauthenticated"}', 'Unable to send the SMS: "Unknown reason".'];
81+
}
82+
}

0 commit comments

Comments
 (0)
0