Skip to content

Commit c1c2409

Browse files
feat: add Messenger class for multiple adapter failover support
This commit introduces the Messenger class, which enables automatic failover across multiple messaging adapters. If one adapter throws an exception, the next adapter in the sequence is tried until one succeeds or all fail. Features: - Accepts a single Adapter or an array of Adapters - Tries adapters sequentially on exception - Validates adapter compatibility (same type and message type) - Returns the first successful response - Throws aggregated exception with details if all adapters fail - Supports SMS, Email, Push, and any other adapter types Example usage: $messenger = new Messenger([ new Twilio('sid', 'token'), new Vonage('key', 'secret'), ]); $result = $messenger->send($message); Changes: - Add src/Utopia/Messaging/Messenger.php - Add tests/Messaging/MessengerTest.php with comprehensive test coverage - Update README.md with usage example Closes: feature request for multiple adapter support
1 parent a6ac04f commit c1c2409

3 files changed

Lines changed: 567 additions & 0 deletions

File tree

README.md

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

82+
## Multiple Adapters (Failover)
83+
84+
You can use multiple adapters with automatic failover. If one adapter throws an exception, the next one will be tried.
85+
86+
```php
87+
<?php
88+
89+
use \Utopia\Messaging\Messenger;
90+
use \Utopia\Messaging\Messages\SMS;
91+
use \Utopia\Messaging\Adapter\SMS\Twilio;
92+
use \Utopia\Messaging\Adapter\SMS\Vonage;
93+
94+
$message = new SMS(
95+
to: ['+12025550139'],
96+
content: 'Hello World'
97+
);
98+
99+
$messenger = new Messenger([
100+
new Twilio('YOUR_ACCOUNT_SID', 'YOUR_AUTH_TOKEN'),
101+
new Vonage('YOUR_API_KEY', 'YOUR_API_SECRET'),
102+
]);
103+
104+
$messenger->send($message);
105+
```
106+
107+
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.
108+
82109
## Adapters
83110

84111
> 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: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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 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+
$this->validateAdapters($adapters);
53+
54+
$this->adapters = $adapters;
55+
}
56+
57+
/**
58+
* Send a message using the first available adapter.
59+
*
60+
* Tries each adapter in sequence. If an adapter throws an exception,
61+
* it moves to the next adapter. Returns the result of the first
62+
* successful adapter.
63+
*
64+
* @param Message $message The message to send.
65+
* @return array{
66+
* deliveredTo: int,
67+
* type: string,
68+
* results: array<array<string, mixed>>
69+
* }
70+
*
71+
* @throws \Exception If all adapters fail or if the message type is invalid.
72+
*/
73+
public function send(Message $message): array
74+
{
75+
$errors = [];
76+
$messageType = $this->adapters[0]->getMessageType();
77+
78+
if (! \is_a($message, $messageType)) {
79+
throw new \Exception(sprintf(
80+
'Invalid message type. Expected "%s", got "%s".',
81+
$messageType,
82+
get_class($message)
83+
));
84+
}
85+
86+
foreach ($this->adapters as $index => $adapter) {
87+
try {
88+
return $adapter->send($message);
89+
} catch (\Exception $e) {
90+
$errors[] = sprintf(
91+
'%s (adapter %d): %s',
92+
$adapter->getName(),
93+
$index + 1,
94+
$e->getMessage()
95+
);
96+
}
97+
}
98+
99+
throw new \Exception(sprintf(
100+
"All %d adapters failed:\n%s",
101+
count($this->adapters),
102+
implode("\n", $errors)
103+
));
104+
}
105+
106+
/**
107+
* Get the message type supported by this messenger.
108+
*
109+
* All adapters must support the same message type.
110+
*/
111+
public function getMessageType(): string
112+
{
113+
return $this->adapters[0]->getMessageType();
114+
}
115+
116+
/**
117+
* Get the adapter type (sms, email, push, etc.).
118+
*
119+
* All adapters must be of the same type.
120+
*/
121+
public function getType(): string
122+
{
123+
return $this->adapters[0]->getType();
124+
}
125+
126+
/**
127+
* Get the maximum number of messages that can be sent in a single request.
128+
*
129+
* Returns the minimum maxMessagesPerRequest of all adapters to ensure
130+
* the messenger never accepts a message that any adapter cannot handle.
131+
*/
132+
public function getMaxMessagesPerRequest(): int
133+
{
134+
return array_reduce(
135+
$this->adapters,
136+
fn ($min, $adapter) => min($min, $adapter->getMaxMessagesPerRequest()),
137+
PHP_INT_MAX
138+
);
139+
}
140+
141+
/**
142+
* Validate that all adapters are compatible.
143+
*
144+
* @param array<Adapter> $adapters
145+
*
146+
* @throws \InvalidArgumentException If adapters are not compatible.
147+
*/
148+
private function validateAdapters(array $adapters): void
149+
{
150+
$firstAdapter = $adapters[0];
151+
$expectedType = $firstAdapter->getType();
152+
$expectedMessageType = $firstAdapter->getMessageType();
153+
154+
foreach ($adapters as $index => $adapter) {
155+
if ($adapter->getType() !== $expectedType) {
156+
throw new \InvalidArgumentException(sprintf(
157+
'All adapters must be of the same type. Expected "%s", but adapter %d (%s) has type "%s".',
158+
$expectedType,
159+
$index + 1,
160+
$adapter->getName(),
161+
$adapter->getType()
162+
));
163+
}
164+
165+
if ($adapter->getMessageType() !== $expectedMessageType) {
166+
throw new \InvalidArgumentException(sprintf(
167+
'All adapters must support the same message type. Expected "%s", but adapter %d (%s) supports "%s".',
168+
$expectedMessageType,
169+
$index + 1,
170+
$adapter->getName(),
171+
$adapter->getMessageType()
172+
));
173+
}
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)