Skip to content

Commit d4b60e4

Browse files
author
deepshekhardas
committed
feat: add Messenger class for multiple adapter failover
Based on PR #115 by TorstenDittmann. Adds Messenger class that orchestrates multiple adapters with automatic failover.
1 parent 1c82b99 commit d4b60e4

3 files changed

Lines changed: 629 additions & 0 deletions

File tree

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,33 @@ $messaging = new FCM('YOUR_SERVICE_ACCOUNT_JSON');
8383
$messaging->send($message);
8484
```
8585

86+
## Multiple Adapters (Failover)
87+
88+
You can use multiple adapters with automatic failover. If one adapter throws an exception, the next one will be tried.
89+
90+
```php
91+
<?php
92+
93+
use \Utopia\Messaging\Messenger;
94+
use \Utopia\Messaging\Messages\SMS;
95+
use \Utopia\Messaging\Adapter\SMS\Twilio;
96+
use \Utopia\Messaging\Adapter\SMS\Vonage;
97+
98+
$message = new SMS(
99+
to: ['+12025550139'],
100+
content: 'Hello World'
101+
);
102+
103+
$messenger = new Messenger([
104+
new Twilio('YOUR_ACCOUNT_SID', 'YOUR_AUTH_TOKEN'),
105+
new Vonage('YOUR_API_KEY', 'YOUR_API_SECRET'),
106+
]);
107+
108+
$messenger->send($message);
109+
```
110+
111+
The `Messenger` class accepts multiple adapters and tries them in order. It stops at the first successful response and only throws an exception if all adapters fail.
112+
86113
## Adapters
87114

88115
> Want to implement any of the missing adapters or have an idea for another? We would love to hear from you! Please check out our [contribution guide](./CONTRIBUTING.md) and [new adapter guide](./docs/add-new-adapter.md) for more information.

src/Utopia/Messaging/Messenger.php

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php
2+
3+
namespace Utopia\Messaging;
4+
5+
/**
6+
* A messenger that orchestrates multiple adapters for failover.
7+
*
8+
* This class accepts multiple adapters and tries them in sequence.
9+
* If one adapter throws an exception, it will try the next one.
10+
* It stops at the first successful response.
11+
*
12+
* Example usage:
13+
* ```php
14+
* use Utopia\Messaging\Messenger;
15+
* use Utopia\Messaging\Adapter\SMS\Twilio;
16+
* use Utopia\Messaging\Adapter\SMS\Vonage;
17+
* use Utopia\Messaging\Messages\SMS;
18+
*
19+
* $messenger = new Messenger([
20+
* new Twilio('sid', 'token'),
21+
* new Vonage('key', 'secret'),
22+
* ]);
23+
*
24+
* $message = new SMS(to: ['+1234567890'], content: 'Hello!');
25+
* $result = $messenger->send($message);
26+
* ```
27+
*/
28+
class Messenger
29+
{
30+
/**
31+
* @var array<Adapter>
32+
*/
33+
private array $adapters;
34+
35+
/**
36+
* @param Adapter|array<Adapter> $adapters An adapter or array of adapters to try in sequence.
37+
* At least one adapter must be provided.
38+
* All adapters must support the same message type.
39+
*
40+
* @throws \InvalidArgumentException If no adapters are provided, an array element is not an adapter, or adapters have mixed types.
41+
*/
42+
public function __construct(Adapter|array $adapters)
43+
{
44+
if ($adapters instanceof Adapter) {
45+
$adapters = [$adapters];
46+
}
47+
48+
if (empty($adapters)) {
49+
throw new \InvalidArgumentException('At least one adapter must be provided.');
50+
}
51+
52+
foreach ($adapters as $index => $adapter) {
53+
if (! $adapter instanceof Adapter) {
54+
throw new \InvalidArgumentException(
55+
'All elements must be instances of Adapter, but element '
56+
.$index
57+
.' is '
58+
.\get_debug_type($adapter)
59+
.'.'
60+
);
61+
}
62+
}
63+
64+
$this->validateAdapters($adapters);
65+
66+
$this->adapters = $adapters;
67+
}
68+
69+
/**
70+
* Send a message using the first available adapter.
71+
*
72+
* Tries each adapter in sequence. If an adapter throws an exception,
73+
* it moves to the next adapter. Returns the result of the first
74+
* successful adapter.
75+
*
76+
* @param Message $message The message to send.
77+
* @return array{
78+
* deliveredTo: int,
79+
* type: string,
80+
* results: array<array<string, mixed>>
81+
* }
82+
*
83+
* @throws \Exception If all adapters fail or if the message type is invalid.
84+
*/
85+
public function send(Message $message): array
86+
{
87+
$errors = [];
88+
$messageType = $this->adapters[0]->getMessageType();
89+
90+
if (! \is_a($message, $messageType)) {
91+
throw new \Exception(
92+
'Invalid message type. Expected "'
93+
.$messageType
94+
.'", got "'
95+
.\get_class($message)
96+
.'".'
97+
);
98+
}
99+
100+
foreach ($this->adapters as $index => $adapter) {
101+
try {
102+
return $adapter->send($message);
103+
} catch (\Exception $e) {
104+
$errors[] = $adapter->getName()
105+
.' (adapter '
106+
.($index + 1)
107+
.'): '
108+
.$e->getMessage();
109+
}
110+
}
111+
112+
$adapterCount = \count($this->adapters);
113+
$adapterLabel = $adapterCount === 1 ? 'adapter' : 'adapters';
114+
115+
throw new \Exception(
116+
'All '
117+
.$adapterCount
118+
.' '
119+
.$adapterLabel
120+
." failed:\n"
121+
.\implode("\n", $errors)
122+
);
123+
}
124+
125+
/**
126+
* Get the message type supported by this messenger.
127+
*
128+
* All adapters must support the same message type.
129+
*/
130+
public function getMessageType(): string
131+
{
132+
return $this->adapters[0]->getMessageType();
133+
}
134+
135+
/**
136+
* Get the adapter type (sms, email, push, etc.).
137+
*
138+
* All adapters must be of the same type.
139+
*/
140+
public function getType(): string
141+
{
142+
return $this->adapters[0]->getType();
143+
}
144+
145+
/**
146+
* Get the maximum number of messages that can be sent in a single request.
147+
*
148+
* Returns the minimum maxMessagesPerRequest of all adapters to ensure
149+
* the messenger never accepts a message that any adapter cannot handle.
150+
*/
151+
public function getMaxMessagesPerRequest(): int
152+
{
153+
return array_reduce(
154+
$this->adapters,
155+
fn ($min, $adapter) => min($min, $adapter->getMaxMessagesPerRequest()),
156+
PHP_INT_MAX
157+
);
158+
}
159+
160+
/**
161+
* Validate that all adapters are compatible.
162+
*
163+
* @param array<Adapter> $adapters
164+
*
165+
* @throws \InvalidArgumentException If adapters are not compatible.
166+
*/
167+
private function validateAdapters(array $adapters): void
168+
{
169+
$firstAdapter = $adapters[0];
170+
$expectedType = $firstAdapter->getType();
171+
$expectedMessageType = $firstAdapter->getMessageType();
172+
173+
foreach (\array_slice($adapters, 1, preserve_keys: true) as $index => $adapter) {
174+
if ($adapter->getType() !== $expectedType) {
175+
throw new \InvalidArgumentException(
176+
'All adapters must be of the same type. Expected "'
177+
.$expectedType
178+
.'", but adapter '
179+
.($index + 1)
180+
.' ('
181+
.$adapter->getName()
182+
.') has type "'
183+
.$adapter->getType()
184+
.'".'
185+
);
186+
}
187+
188+
if ($adapter->getMessageType() !== $expectedMessageType) {
189+
throw new \InvalidArgumentException(
190+
'All adapters must support the same message type. Expected "'
191+
.$expectedMessageType
192+
.'", but adapter '
193+
.($index + 1)
194+
.' ('
195+
.$adapter->getName()
196+
.') supports "'
197+
.$adapter->getMessageType()
198+
.'".'
199+
);
200+
}
201+
}
202+
}
203+
}

0 commit comments

Comments
 (0)